mcpField
P
pyside6-qml-architecture

pyside6-qml-architecture

github.com/ds-codi開発47 回インストール
リポジトリ

pyside6-qml-architecture について

Python、PySide6、QMLを使うデスクトップアプリ向けに、MVC構成やプロジェクト構造、各層の責務を定めるスキルです。起動と依存性注入のパターン、シグナルフロー、設計上の制約を扱います。

主な機能

  1. 01MVC各層の責務定義
  2. 02Python-QMLブリッジの役割指定
  3. 03起動と依存性注入のパターン
  4. 04シグナルとプロパティバインディングの通信説明
  5. 05プロジェクト構成とファイル分割の指針

活用シーン

  1. 01PySide6 QMLアプリの構成計画
  2. 02ビュー、コントローラー、サービスの境界整理
  3. 03起動と層間通信の設計

インストール

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

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

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

詳細

PySide6 QML MVC アーキテクチャ

このワークスペースのデスクトップ GUI アプリケーションでは、Python + PySide6 と、ビュー層に QML ファイルを使用し、厳格な Model-View-Controller(MVC)アーキテクチャに従っています。このスキルでは、ds_pas/ アプリケーションに基づく標準的なプロジェクト構成、ブートストラップパターン、各層の責務について説明します。

アーキテクチャの概要

┌─────────────────────────────────────────┐
│           View Layer (QML files)        │
│  Declarative UI, data binding, signals  │
└──────────────────┬──────────────────────┘
                   │ Properties, Signals, Slots
┌──────────────────▼──────────────────────┐
│         Python-QML Bridge               │
│  QObject subclasses exposed to QML      │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│           Controller Layer              │
│  Coordinate models & services           │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│           Model Layer                   │
│  Data structures, validation, signals   │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│           Services Layer                │
│  Database, files, network, broker       │
└─────────────────────────────────────────┘

プロジェクト構成

my_app/
├── app.py                    # Bootstrap & DI container (singleton)
├── __init__.py               # Package init
├── __main__.py               # Entry point: python -m my_app
├── controllers/
│   ├── __init__.py
│   ├── base.py               # BaseController with view registry
│   └── *_controller.py       # Domain controllers (job, settings, etc.)
├── models/
│   ├── __init__.py
│   ├── base.py               # BaseModel with property_changed signal
│   ├── state.py              # ApplicationState singleton
│   └── *.py                  # Domain models (job, piece, customer, etc.)
├── views/
│   ├── __init__.py
│   ├── bridge.py             # QObject bridge classes exposed to QML
│   ├── main_window.py        # Main window setup (QQmlApplicationEngine)
│   └── components/           # Reusable Python view helpers
├── services/
│   ├── __init__.py
│   ├── database/             # Repository pattern for data access
│   └── *.py                  # External interaction services
├── resources/
│   ├── qml/
│   │   ├── main.qml          # Root QML component
│   │   ├── components/       # Reusable QML components
│   │   ├── pages/            # Page-level QML views
│   │   └── styles/           # Theme and style definitions
│   ├── icons/                # SVG/PNG icons
│   └── qml.qrc              # Qt resource file (optional)
├── utils/
│   ├── __init__.py
│   ├── signals.py            # Central SignalRegistry
│   ├── types.py              # Type aliases, protocols, ServiceLocator
│   └── paths.py              # Path resolution helpers
└── tests/
    └── *.py

各層の責務

コンポーネント責務禁止事項
Modelデータ構造、検証、シリアル化、property_changed シグナルUI に触れる、サービスを呼び出す、QML を参照する
View (QML)宣言的な UI レイアウト、ブリッジのプロパティへのデータバインディング、ユーザー入力の取得ビジネスロジックを含める、サービスを直接呼び出す
Bridgeモデルデータとコントローラーのアクションを QML に公開する QObject サブクラスビジネスロジックを含める、QML を直接操作する
Controllerモデルとサービスの調整、アクションの処理、シグナルの送出UI を直接操作する、QML 型をインポートする
Serviceデータベースクエリ、ファイル I/O、ネットワーク呼び出し、IPCモデル、ビュー、コントローラーを参照する

アプリケーションのブートストラップ(DI コンテナー)

アプリケーションクラスは、すべての層を連携させるシングルトンです。

"""app.py — Bootstrap & DI container."""
import sys
import logging
from typing import Any
from pathlib import Path

from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QObject, QUrl

from my_app.utils.signals import SignalRegistry, get_signal_registry
from my_app.utils.types import ServiceLocator

logger = logging.getLogger(__name__)


class MyApplication:
    """Singleton application with DI container."""
    
    _instance: "MyApplication | None" = None

    def __new__(cls, dev_mode: bool = False) -> "MyApplication":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self, dev_mode: bool = False) -> None:
        if self._initialized:
            return
        self._initialized = True
        self._dev_mode = dev_mode
        self._qt_app: QApplication | None = None
        self._engine: QQmlApplicationEngine | None = None
        self._services: dict[str, Any] = {}
        self._controllers: dict[str, Any] = {}
        self._signals = get_signal_registry()
        self._service_locator = ServiceLocator()

    def _register_services(self) -> None:
        """Create and register all service instances."""
        # self._services["db"] = DatabaseService(...)
        pass

    def _register_controllers(self) -> None:
        """Create controllers with injected dependencies."""
        # self._controllers["job"] = JobController(
        #     signals=self._signals,
        #     repository=self._services["db"],
        # )
        pass

    def _register_qml_types(self) -> None:
        """Register Python bridge objects as QML context properties."""
        ctx = self._engine.rootContext()
        # ctx.setContextProperty("jobBridge", self._bridges["job"])
        pass

    def run(self) -> int:
        self._qt_app = QApplication(sys.argv)
        self._register_services()
        self._register_controllers()

        self._engine = QQmlApplicationEngine()
        self._register_qml_types()

        qml_path = Path(__file__).parent / "resources" / "qml" / "main.qml"
        self._engine.load(QUrl.fromLocalFile(str(qml_path)))

        if not self._engine.rootObjects():
            return -1

        return self._qt_app.exec()

エントリーポイント

"""__main__.py"""
import sys
from my_app.app import MyApplication

def main():
    app = MyApplication(dev_mode="--dev" in sys.argv)
    sys.exit(app.run())

if __name__ == "__main__":
    main()

ServiceLocator パターン

"""utils/types.py — Type aliases, protocols, and DI container."""
from typing import Any, Protocol, TypeVar, runtime_checkable

T = TypeVar("T")

@runtime_checkable
class IController(Protocol):
    def initialize(self) -> None: ...
    def cleanup(self) -> None: ...

@runtime_checkable
class IService(Protocol):
    def initialize(self) -> None: ...
    def shutdown(self) -> None: ...

class ServiceLocator:
    """Lightweight DI container for service instances."""
    _instance: "ServiceLocator | None" = None

    def __new__(cls) -> "ServiceLocator":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._services = {}
        return cls._instance

    def register(self, name: str, service: Any) -> None:
        self._services[name] = service

    def get(self, name: str) -> Any:
        if name not in self._services:
            raise KeyError(f"Service '{name}' not registered")
        return self._services[name]

    def has(self, name: str) -> bool:
        return name in self._services

シグナルの流れ

User Action (QML)
       ↓
  QML emits signal / calls slot on Bridge
       ↓
  Bridge delegates to Controller
       ↓
  Controller calls Service
       ↓
  Service returns result
       ↓
  Controller updates Model
       ↓
  Model emits property_changed
       ↓
  Bridge property notifies QML (via NOTIFY)
       ↓
  QML binding automatically updates UI

主な設計ルール

  1. QML ファイルは宣言のみを記述します — JavaScript のビジネスロジックや、サービスの直接呼び出しは禁止です
  2. Python ブリッジオブジェクトは、QML と Python バックエンド間の唯一のインターフェースです
  3. コントローラーは QML 型をインポートしません — ブリッジのシグナル/プロパティを通じて処理します
  4. モデルは純粋なデータです — UI のインポートやサービスの呼び出しは禁止です
  5. サービスはステートレスな処理役です — モデルへの参照や UI に関する知識は持ちません
  6. 層をまたぐすべての通信は、SignalRegistry または Qt のプロパティバインディングを通じて行います
  7. シングルトン(Application、SignalRegistry、ServiceLocator、ApplicationState)は、スレッドセーフに再利用するために __new__ パターンを使用します

ファイルサイズのガイドライン

  • ファイルは単一の責務に集中させます
  • 約300~400行を超えるファイルは、サブモジュールに分割します
  • 再利用可能な QML コンポーネントは resources/qml/components/ に切り出します
  • 関連するコントローラー/サービスは、ドメインごとにまとめます(例:controllers/job_controller.py)

参考資料

コメント