pyside6-qml-views
SKILL.md
PySide6 QML Views
All UI in this architecture is defined declaratively in .qml files. QML views bind to Python bridge properties and call bridge slots — they contain no business logic.
QML File Organization
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)
Root Window (main.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:
// 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
// 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
// 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
// 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
// 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)
// styles/qmldir
module Styles
singleton Theme 1.0 Theme.qml
Usage
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
// 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
// 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
| Rule | Rationale |
|---|---|
| Keep components under 150 lines | Maintainability; extract sub-components |
| One root item per file | QML convention |
Use id only when referenced |
Avoid unnecessary identity |
| Prefer property bindings over imperative JS | Declarative updates, fewer bugs |
Use Loader for heavy / conditional content |
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 (root.width vs width) |
Avoid shadowing in nested items |
| Use anchors or layouts, not manual x/y | Responsive and maintainable |
Resource Loading
Icons from filesystem
Image {
source: "file:///" + Qt.resolvedUrl("../../icons/logo.svg")
}
Icons via Qt Resource System
Image {
source: "qrc:/icons/logo.svg"
}
Qt Resource File (qml.qrc)
<RCC>
<qresource prefix="/">
<file>qml/main.qml</file>
<file>qml/components/ActionButton.qml</file>
<file>icons/logo.svg</file>
</qresource>
</RCC>
References
Weekly Installs
27
Repository
ds-codi/project…mory-mcpGitHub Stars
3
First Seen
Feb 27, 2026
Security Audits
Installed on
opencode27
gemini-cli27
github-copilot27
codex27
amp27
cline27