mcpField
P
pyside6-qml-architecture

pyside6-qml-architecture

github.com/ds-codi开发47 次安装
代码仓库

关于 pyside6-qml-architecture

为 Python + PySide6 与 QML 桌面应用规定 MVC 架构、项目结构和各层职责,适合构建分层清晰的 GUI 项目。内容涵盖启动与依赖注入模式,以及层间信号流和设计约束。

主要功能

  1. 01规定 MVC 分层职责
  2. 02定义 Python-QML 桥接层职责
  3. 03提供应用启动与依赖注入模式
  4. 04说明信号与属性绑定通信
  5. 05给出项目结构及文件拆分建议

适用场景

  1. 01规划 PySide6 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 文件构建视图层,遵循严格的模型-视图-控制器(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

各层职责

组件职责不得
模型数据结构、验证、序列化、property_changed 信号操作 UI、调用服务、引用 QML
视图(QML)声明式 UI 布局、将数据绑定到桥接对象属性、捕获用户输入包含业务逻辑、直接调用服务
桥接对象向 QML 暴露模型数据和控制器操作的 QObject 子类包含业务逻辑、直接操作 QML
控制器协调模型和服务、处理操作、发出信号直接操作 UI、导入 QML 类型
服务数据库查询、文件 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)

参考资料

评论

支持 Markdown