mcpField
P
pyside6-qml-views

pyside6-qml-views

github.com/ds-codiデザイン・UI49 回インストール
リポジトリ

pyside6-qml-views について

pyside6-qml-viewsは、PySide6とQMLで画面を構築するための設計スキルです。表示とビジネスロジックを分離したい開発者向けに、ページ、再利用可能なコンポーネント、テーマ、読み込み状態、リソースの構成を扱います。

主な機能

  1. 01QMLによる宣言的な画面定義
  2. 02Pythonブリッジプロパティへのページ連携
  3. 03ボタンや検索バーなどの再利用
  4. 04テーマシングルトンによるスタイル設定
  5. 05Loaderによる条件付きコンテンツ読み込み
  6. 06リソースシステムでのアイコン読み込み

活用シーン

  1. 01複数ページのデスクトップ画面構築
  2. 02再利用可能なQMLコントロールの作成
  3. 03画面表示とI/Oロジックの分離

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ds-codi/project-memory-mcp --skill pyside6-qml-views

インストール後、Claude Code で/pyside6-qml-viewsを入力して読み込まれたか確認します。

詳細

PySide6 QML ビュー

このアーキテクチャのすべての UI は、.qml ファイルで宣言的に定義されています。QML ビューは Python ブリッジのプロパティにバインドし、ブリッジのスロットを呼び出します。ビジネスロジックは含みません。

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)

ルートウィンドウ(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
    }
}

ページのパターン

各ページは、ブリッジのプロパティにバインドする自己完結型の 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
    }
}

再利用可能なコンポーネントのパターン

コンポーネントのファイル構成
// 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
    }
}
カスタムシグナルを持つコンポーネント
// 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)
    }
}

ダイアログのパターン

// 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
        }
    }
}

テーマ/スタイル設定

テーマシングルトン
// 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(モジュール登録)
// styles/qmldir
module Styles
singleton Theme 1.0 Theme.qml
使用方法
import "styles" as Styles

Rectangle {
    color: Styles.Theme.surface
    radius: Styles.Theme.cardRadius

    Label {
        color: Styles.Theme.textPrimary
        font.pixelSize: Styles.Theme.fontSizeMedium
    }
}

読み込み状態

// 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
    }
}

ステータスインジケーター

// 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 のベストプラクティス

ルール理由
コンポーネントは 150 行未満に保つ保守性を高めます。サブコンポーネントを切り出します
ファイルごとにルートアイテムを 1 つにするQML の慣例です
参照する場合にのみ id を使う不要な識別子を避けます
命令型 JS よりプロパティバインディングを優先する宣言的な更新ができ、バグが減ります
重いコンテンツや条件付きコンテンツには Loader を使う遅延インスタンス化によりメモリを節約できます
QML の JS に SQL、HTTP、ファイル I/O を埋め込まないすべての副作用はブリッジのスロットを経由させます
プロパティアクセスを明示する(root.width と width の使い分け)ネストされたアイテムでの名前の衝突を避けます
手動の x/y 指定ではなく、アンカーまたはレイアウトを使うレスポンシブで保守しやすくなります

リソースの読み込み

ファイルシステムからのアイコン読み込み
Image {
    source: "file:///" + Qt.resolvedUrl("../../icons/logo.svg")
}
Qt リソースシステム経由のアイコン読み込み
Image {
    source: "qrc:/icons/logo.svg"
}
Qt リソースファイル(qml.qrc)
<RCC>
    <qresource prefix="/">
        <file>qml/main.qml</file>
        <file>qml/components/ActionButton.qml</file>
        <file>icons/logo.svg</file>
    </qresource>
</RCC>

参考資料

コメント