pyside6-qml-views
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePySide6 QML Views
PySide6 QML 视图
All UI in this architecture is defined declaratively in files. QML views bind to Python bridge properties and call bridge slots — they contain no business logic.
.qml本架构中的所有UI均通过文件声明式定义。QML视图绑定到Python桥接属性并调用桥接槽函数——它们不包含任何业务逻辑。
.qmlQML File Organization
QML 文件组织结构
resources/
├── qml/
│ ├── main.qml # Root window / StackLayout host
│ ├── components/
│ │ ├── ActionButton.qml # Reusable styled button
│ │ ├── StatusBadge.qml # Status indicator
│ │ ├── SearchBar.qml # Search input with debounce
│ │ ├── LoadingOverlay.qml # Busy spinner overlay
│ │ └── ErrorBanner.qml # Error message bar
│ ├── pages/
│ │ ├── JobListPage.qml # Job listing with cards
│ │ ├── JobDetailPage.qml # Single job detail view
│ │ ├── SettingsPage.qml # App settings form
│ │ └── DashboardPage.qml # Overview / landing page
│ ├── dialogs/
│ │ ├── CreateJobDialog.qml # Modal dialog for new job
│ │ └── ConfirmDialog.qml # Generic confirmation popup
│ └── styles/
│ ├── Theme.qml # Colour palette, spacing, fonts
│ └── qmldir # Module metadata for imports
├── icons/
│ ├── *.svg # Vector icons
│ └── *.png # Raster icons
└── qml.qrc # Qt resource file (optional)resources/
├── qml/
│ ├── main.qml # 根窗口 / StackLayout 宿主
│ ├── components/
│ │ ├── ActionButton.qml # 可复用样式按钮
│ │ ├── StatusBadge.qml # 状态指示器
│ │ ├── SearchBar.qml # 带防抖的搜索输入框
│ │ ├── LoadingOverlay.qml # 加载中遮罩层
│ │ └── ErrorBanner.qml # 错误提示栏
│ ├── pages/
│ │ ├── JobListPage.qml # 带卡片的任务列表页
│ │ ├── JobDetailPage.qml # 单个任务详情页
│ │ ├── SettingsPage.qml # 应用设置表单页
│ │ └── DashboardPage.qml # 概览/首页
│ ├── dialogs/
│ │ ├── CreateJobDialog.qml # 新建任务模态对话框
│ │ └── ConfirmDialog.qml # 通用确认弹窗
│ └── styles/
│ ├── Theme.qml # 调色板、间距、字体
│ └── qmldir # 用于导入的模块元数据
├── icons/
│ ├── *.svg # 矢量图标
│ └── *.png # 栅格图标
└── qml.qrc # Qt 资源文件(可选)Root Window (main.qml)
根窗口 (main.qml)
qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
id: root
visible: true
width: 1280
height: 720
title: "My Application"
// Page navigation
StackLayout {
id: pageStack
anchors.fill: parent
currentIndex: 0
JobListPage {}
JobDetailPage {}
SettingsPage {}
}
// Global toolbar
header: ToolBar {
RowLayout {
anchors.fill: parent
Label {
text: "My App"
font.bold: true
Layout.leftMargin: 12
}
Item { Layout.fillWidth: true }
ToolButton {
text: "Jobs"
onClicked: pageStack.currentIndex = 0
}
ToolButton {
text: "Settings"
onClicked: pageStack.currentIndex = 2
}
}
}
// Global error banner
ErrorBanner {
id: errorBanner
anchors { top: parent.top; left: parent.left; right: parent.right }
visible: jobBridge.errorMessage !== ""
message: jobBridge.errorMessage
}
// Loading overlay
LoadingOverlay {
anchors.fill: parent
visible: jobBridge.isBusy
}
}qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
id: root
visible: true
width: 1280
height: 720
title: "My Application"
// Page navigation
StackLayout {
id: pageStack
anchors.fill: parent
currentIndex: 0
JobListPage {}
JobDetailPage {}
SettingsPage {}
}
// Global toolbar
header: ToolBar {
RowLayout {
anchors.fill: parent
Label {
text: "My App"
font.bold: true
Layout.leftMargin: 12
}
Item { Layout.fillWidth: true }
ToolButton {
text: "Jobs"
onClicked: pageStack.currentIndex = 0
}
ToolButton {
text: "Settings"
onClicked: pageStack.currentIndex = 2
}
}
}
// Global error banner
ErrorBanner {
id: errorBanner
anchors { top: parent.top; left: parent.left; right: parent.right }
visible: jobBridge.errorMessage !== ""
message: jobBridge.errorMessage
}
// Loading overlay
LoadingOverlay {
anchors.fill: parent
visible: jobBridge.isBusy
}
}Page Pattern
页面模式
Every page is a self-contained QML file that binds to bridge properties:
qml
// pages/JobListPage.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Page {
id: jobListPage
header: ToolBar {
RowLayout {
anchors.fill: parent
SearchBar {
id: searchBar
Layout.fillWidth: true
onSearchTriggered: jobBridge.searchJobs(query)
}
ActionButton {
text: "New Job"
icon.name: "add"
onClicked: createJobDialog.open()
}
}
}
ListView {
id: jobsListView
anchors.fill: parent
model: jobListModel
spacing: 4
clip: true
delegate: ItemDelegate {
width: jobsListView.width
height: 64
contentItem: RowLayout {
spacing: 12
Label {
text: model.jobNumber
font.bold: true
Layout.preferredWidth: 100
}
Label {
text: model.jobName
Layout.fillWidth: true
elide: Text.ElideRight
}
StatusBadge {
status: model.status
}
}
onClicked: {
jobBridge.activateJob(model.jobNumber)
pageStack.currentIndex = 1 // navigate to detail
}
}
// Empty state
Label {
anchors.centerIn: parent
visible: jobsListView.count === 0
text: "No jobs found"
opacity: 0.5
}
}
CreateJobDialog {
id: createJobDialog
}
}每个页面都是一个独立的QML文件,绑定到桥接属性:
qml
// pages/JobListPage.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Page {
id: jobListPage
header: ToolBar {
RowLayout {
anchors.fill: parent
SearchBar {
id: searchBar
Layout.fillWidth: true
onSearchTriggered: jobBridge.searchJobs(query)
}
ActionButton {
text: "New Job"
icon.name: "add"
onClicked: createJobDialog.open()
}
}
}
ListView {
id: jobsListView
anchors.fill: parent
model: jobListModel
spacing: 4
clip: true
delegate: ItemDelegate {
width: jobsListView.width
height: 64
contentItem: RowLayout {
spacing: 12
Label {
text: model.jobNumber
font.bold: true
Layout.preferredWidth: 100
}
Label {
text: model.jobName
Layout.fillWidth: true
elide: Text.ElideRight
}
StatusBadge {
status: model.status
}
}
onClicked: {
jobBridge.activateJob(model.jobNumber)
pageStack.currentIndex = 1 // navigate to detail
}
}
// Empty state
Label {
anchors.centerIn: parent
visible: jobsListView.count === 0
text: "No jobs found"
opacity: 0.5
}
}
CreateJobDialog {
id: createJobDialog
}
}Reusable Component Pattern
可复用组件模式
Component File Structure
组件文件结构
qml
// components/ActionButton.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Button {
id: control
// Custom properties
property color accentColor: "#1976D2"
property bool loading: false
enabled: !loading
opacity: enabled ? 1.0 : 0.5
contentItem: Row {
spacing: 8
BusyIndicator {
running: control.loading
visible: control.loading
width: 16; height: 16
}
Label {
text: control.text
color: "white"
verticalAlignment: Text.AlignVCenter
}
}
background: Rectangle {
radius: 4
color: control.down ? Qt.darker(accentColor, 1.2)
: control.hovered ? Qt.lighter(accentColor, 1.1)
: accentColor
}
}qml
// components/ActionButton.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Button {
id: control
// Custom properties
property color accentColor: "#1976D2"
property bool loading: false
enabled: !loading
opacity: enabled ? 1.0 : 0.5
contentItem: Row {
spacing: 8
BusyIndicator {
running: control.loading
visible: control.loading
width: 16; height: 16
}
Label {
text: control.text
color: "white"
verticalAlignment: Text.AlignVCenter
}
}
background: Rectangle {
radius: 4
color: control.down ? Qt.darker(accentColor, 1.2)
: control.hovered ? Qt.lighter(accentColor, 1.1)
: accentColor
}
}Component with Custom Signals
带自定义信号的组件
qml
// components/SearchBar.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
TextField {
id: searchField
signal searchTriggered(string query)
placeholderText: "Search..."
selectByMouse: true
// Debounced search
Timer {
id: debounceTimer
interval: 300
onTriggered: searchField.searchTriggered(searchField.text)
}
onTextChanged: debounceTimer.restart()
onAccepted: {
debounceTimer.stop()
searchTriggered(text)
}
}qml
// components/SearchBar.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
TextField {
id: searchField
signal searchTriggered(string query)
placeholderText: "Search..."
selectByMouse: true
// Debounced search
Timer {
id: debounceTimer
interval: 300
onTriggered: searchField.searchTriggered(searchField.text)
}
onTextChanged: debounceTimer.restart()
onAccepted: {
debounceTimer.stop()
searchTriggered(text)
}
}Dialog Pattern
对话框模式
qml
// dialogs/CreateJobDialog.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Dialog {
id: dialog
title: "Create New Job"
modal: true
anchors.centerIn: Overlay.overlay
width: 400
standardButtons: Dialog.Ok | Dialog.Cancel
onAccepted: {
if (jobNumberInput.text.trim() !== "") {
jobBridge.createJob(jobNumberInput.text.trim())
}
}
onRejected: dialog.close()
// Reset on open
onOpened: {
jobNumberInput.text = ""
jobNumberInput.forceActiveFocus()
}
ColumnLayout {
anchors.fill: parent
spacing: 12
Label { text: "Job Number" }
TextField {
id: jobNumberInput
Layout.fillWidth: true
placeholderText: "e.g. 1234567"
validator: RegularExpressionValidator {
regularExpression: /^\d{5,8}[A-Z]?$/
}
}
Label {
text: "Enter a valid job number (5-8 digits, optional letter suffix)"
font.pixelSize: 11
opacity: 0.6
}
}
}qml
// dialogs/CreateJobDialog.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Dialog {
id: dialog
title: "Create New Job"
modal: true
anchors.centerIn: Overlay.overlay
width: 400
standardButtons: Dialog.Ok | Dialog.Cancel
onAccepted: {
if (jobNumberInput.text.trim() !== "") {
jobBridge.createJob(jobNumberInput.text.trim())
}
}
onRejected: dialog.close()
// Reset on open
onOpened: {
jobNumberInput.text = ""
jobNumberInput.forceActiveFocus()
}
ColumnLayout {
anchors.fill: parent
spacing: 12
Label { text: "Job Number" }
TextField {
id: jobNumberInput
Layout.fillWidth: true
placeholderText: "e.g. 1234567"
validator: RegularExpressionValidator {
regularExpression: /^\d{5,8}[A-Z]?$/
}
}
Label {
text: "Enter a valid job number (5-8 digits, optional letter suffix)"
font.pixelSize: 11
opacity: 0.6
}
}
}Theme / Styling
主题/样式设置
Theme Singleton
单例主题
qml
// styles/Theme.qml
pragma Singleton
import QtQuick 2.15
QtObject {
// Colours
readonly property color primary: "#1976D2"
readonly property color primaryDark: "#1565C0"
readonly property color accent: "#FF9800"
readonly property color background: "#FAFAFA"
readonly property color surface: "#FFFFFF"
readonly property color error: "#D32F2F"
readonly property color textPrimary: "#212121"
readonly property color textSecondary: "#757575"
// Spacing
readonly property int spacingSmall: 4
readonly property int spacingMedium: 8
readonly property int spacingLarge: 16
readonly property int spacingXLarge: 24
// Typography
readonly property int fontSizeSmall: 12
readonly property int fontSizeMedium: 14
readonly property int fontSizeLarge: 18
readonly property int fontSizeTitle: 24
// Elevation / Radii
readonly property int borderRadius: 4
readonly property int cardRadius: 8
}qml
// styles/Theme.qml
pragma Singleton
import QtQuick 2.15
QtObject {
// Colours
readonly property color primary: "#1976D2"
readonly property color primaryDark: "#1565C0"
readonly property color accent: "#FF9800"
readonly property color background: "#FAFAFA"
readonly property color surface: "#FFFFFF"
readonly property color error: "#D32F2F"
readonly property color textPrimary: "#212121"
readonly property color textSecondary: "#757575"
// Spacing
readonly property int spacingSmall: 4
readonly property int spacingMedium: 8
readonly property int spacingLarge: 16
readonly property int spacingXLarge: 24
// Typography
readonly property int fontSizeSmall: 12
readonly property int fontSizeMedium: 14
readonly property int fontSizeLarge: 18
readonly property int fontSizeTitle: 24
// Elevation / Radii
readonly property int borderRadius: 4
readonly property int cardRadius: 8
}qmldir (module registration)
qmldir(模块注册)
// styles/qmldir
module Styles
singleton Theme 1.0 Theme.qml// styles/qmldir
module Styles
singleton Theme 1.0 Theme.qmlUsage
使用方式
qml
import "styles" as Styles
Rectangle {
color: Styles.Theme.surface
radius: Styles.Theme.cardRadius
Label {
color: Styles.Theme.textPrimary
font.pixelSize: Styles.Theme.fontSizeMedium
}
}qml
import "styles" as Styles
Rectangle {
color: Styles.Theme.surface
radius: Styles.Theme.cardRadius
Label {
color: Styles.Theme.textPrimary
font.pixelSize: Styles.Theme.fontSizeMedium
}
}Loading States
加载状态
qml
// components/LoadingOverlay.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle {
id: overlay
color: "#80000000" // semi-transparent black
visible: false
z: 999
BusyIndicator {
anchors.centerIn: parent
running: overlay.visible
width: 48; height: 48
}
MouseArea {
anchors.fill: parent
// Block clicks through overlay
}
}qml
// components/LoadingOverlay.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle {
id: overlay
color: "#80000000" // semi-transparent black
visible: false
z: 999
BusyIndicator {
anchors.centerIn: parent
running: overlay.visible
width: 48; height: 48
}
MouseArea {
anchors.fill: parent
// Block clicks through overlay
}
}Status Indicator
状态指示器
qml
// components/StatusBadge.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle {
id: badge
property string status: ""
width: statusLabel.implicitWidth + 16
height: 24
radius: 12
color: {
switch (status.toLowerCase()) {
case "active": return "#4CAF50";
case "complete": return "#2196F3";
case "on_hold": return "#FF9800";
case "archived": return "#9E9E9E";
default: return "#BDBDBD";
}
}
Label {
id: statusLabel
anchors.centerIn: parent
text: badge.status
color: "white"
font.pixelSize: 11
font.bold: true
}
}qml
// components/StatusBadge.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle {
id: badge
property string status: ""
width: statusLabel.implicitWidth + 16
height: 24
radius: 12
color: {
switch (status.toLowerCase()) {
case "active": return "#4CAF50";
case "complete": return "#2196F3";
case "on_hold": return "#FF9800";
case "archived": return "#9E9E9E";
default: return "#BDBDBD";
}
}
Label {
id: statusLabel
anchors.centerIn: parent
text: badge.status
color: "white"
font.pixelSize: 11
font.bold: true
}
}QML Best Practices
QML 最佳实践
| Rule | Rationale |
|---|---|
| Keep components under 150 lines | Maintainability; extract sub-components |
| One root item per file | QML convention |
Use | Avoid unnecessary identity |
| Prefer property bindings over imperative JS | Declarative updates, fewer bugs |
Use | Lazy instantiation saves memory |
| Never embed SQL, HTTP, or file I/O in QML JS | All side-effects go through bridge slots |
Qualify property access ( | Avoid shadowing in nested items |
| Use anchors or layouts, not manual x/y | Responsive and maintainable |
| 规则 | 理由 |
|---|---|
| 组件代码不超过150行 | 提升可维护性;提取子组件 |
| 每个文件一个根元素 | QML 约定 |
仅在需要引用时使用 | 避免不必要的标识 |
| 优先使用属性绑定而非命令式JS | 声明式更新,减少bug |
对大型/条件性内容使用 | 延迟实例化节省内存 |
| 绝不在QML JS中嵌入SQL、HTTP或文件I/O | 所有副作用都通过桥接槽函数处理 |
限定属性访问( | 避免嵌套元素中的属性遮蔽 |
| 使用锚点或布局,而非手动设置x/y | 响应式且易于维护 |
Resource Loading
资源加载
Icons from filesystem
从文件系统加载图标
qml
Image {
source: "file:///" + Qt.resolvedUrl("../../icons/logo.svg")
}qml
Image {
source: "file:///" + Qt.resolvedUrl("../../icons/logo.svg")
}Icons via Qt Resource System
通过Qt资源系统加载图标
qml
Image {
source: "qrc:/icons/logo.svg"
}qml
Image {
source: "qrc:/icons/logo.svg"
}Qt Resource File (qml.qrc)
Qt资源文件 (qml.qrc)
xml
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/components/ActionButton.qml</file>
<file>icons/logo.svg</file>
</qresource>
</RCC>xml
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/components/ActionButton.qml</file>
<file>icons/logo.svg</file>
</qresource>
</RCC>