pyside6-qml-architecture について
Python、PySide6、QMLを使うデスクトップアプリ向けに、MVC構成やプロジェクト構造、各層の責務を定めるスキルです。起動と依存性注入のパターン、シグナルフロー、設計上の制約を扱います。
主な機能
- 01MVC各層の責務定義
- 02Python-QMLブリッジの役割指定
- 03起動と依存性注入のパターン
- 04シグナルとプロパティバインディングの通信説明
- 05プロジェクト構成とファイル分割の指針
活用シーン
- 01PySide6 QMLアプリの構成計画
- 02ビュー、コントローラー、サービスの境界整理
- 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
主な設計ルール
- QML ファイルは宣言のみを記述します — JavaScript のビジネスロジックや、サービスの直接呼び出しは禁止です
- Python ブリッジオブジェクトは、QML と Python バックエンド間の唯一のインターフェースです
- コントローラーは QML 型をインポートしません — ブリッジのシグナル/プロパティを通じて処理します
- モデルは純粋なデータです — UI のインポートやサービスの呼び出しは禁止です
- サービスはステートレスな処理役です — モデルへの参照や UI に関する知識は持ちません
- 層をまたぐすべての通信は、
SignalRegistryまたは Qt のプロパティバインディングを通じて行います - シングルトン(
Application、SignalRegistry、ServiceLocator、ApplicationState)は、スレッドセーフに再利用するために__new__パターンを使用します
ファイルサイズのガイドライン
- ファイルは単一の責務に集中させます
- 約300~400行を超えるファイルは、サブモジュールに分割します
- 再利用可能な QML コンポーネントは
resources/qml/components/に切り出します - 関連するコントローラー/サービスは、ドメインごとにまとめます(例:
controllers/job_controller.py)
参考資料
コメント
Markdown 対応