mcpField
P
pyside6-mvc

pyside6-mvc

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

关于 pyside6-mvc

为使用 PySide6 构建 Python 桌面应用提供架构指导,面向需要严格 MVC 分层并通过 Qt Designer 文件定义界面的开发者。

主要功能

  1. 01严格区分模型、视图与控制器
  2. 02从 .ui 文件加载界面
  3. 03基于信号的组件通信
  4. 04集中式信号注册表
  5. 05通过依赖注入启动应用

适用场景

  1. 01规划 PySide6 桌面应用结构
  2. 02分离界面、业务逻辑与数据
  3. 03通过共享信号协调视图

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/pyside6-mvc确认已加载。

详细介绍

PySide6 MVC 架构说明

使用 PySide6 构建 Python 桌面应用的指南,采用严格的 MVC 架构,所有 UI 均由 .ui 文件定义。

架构概览

┌─────────────────────────────────────────┐
│           View Layer (.ui files)        │
│  Load from Qt Designer, capture input   │
└──────────────────┬──────────────────────┘
                   │ Signals
┌──────────────────▼──────────────────────┐
│           Controller Layer              │
│  Coordinate models & services           │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│           Model Layer                   │
│  Data structures, validation            │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│           Services Layer                │
│  Database, files, network, broker       │
└─────────────────────────────────────────┘

项目结构

my_app/
├── app.py                    # Bootstrap & DI container
├── __main__.py               # Entry point
├── controllers/
│   ├── base.py               # BaseController
│   └── *_controller.py       # Domain controllers
├── models/
│   ├── base.py               # BaseModel with signals
│   └── *.py                  # Domain models
├── views/
│   ├── base.py               # BaseView
│   └── *.py                  # View classes
├── services/
│   └── *.py                  # External interactions
├── resources/
│   └── ui/                   # .ui files (Qt Designer)
└── utils/
    └── signals.py            # Central signal registry

核心原则

组件职责不做的事
Model数据、验证、序列化操作 UI、调用服务
View加载 .ui 文件、捕获输入包含业务逻辑
Controller协调模型和服务直接操作 UI

基础 Model 模式

from PySide6.QtCore import QObject, Signal
from datetime import datetime
from typing import Any

class BaseModel(QObject):
    property_changed = Signal(str, object)  # name, value
    changed = Signal()
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self._updated_at = datetime.now()
    
    def _set_property(self, name: str, old: Any, new: Any) -> bool:
        if old != new:
            self._updated_at = datetime.now()
            self.property_changed.emit(name, new)
            self.changed.emit()
            return True
        return False
    
    def to_dict(self) -> dict:
        raise NotImplementedError
    
    @classmethod
    def from_dict(cls, data: dict):
        raise NotImplementedError

基础 View 模式

from pathlib import Path
from PySide6.QtWidgets import QWidget, QVBoxLayout
from PySide6.QtCore import QFile, Signal
from PySide6.QtUiTools import QUiLoader

class BaseView(QWidget):
    error_occurred = Signal(str, str)  # title, message
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self._controller = None
        self._init_ui()
        self._connect_signals()
    
    def _load_ui(self, ui_filename: str) -> QWidget:
        ui_path = Path(__file__).parent.parent / "resources" / "ui" / ui_filename
        loader = QUiLoader()
        ui_file = QFile(str(ui_path))
        
        if ui_file.open(QFile.ReadOnly):
            ui = loader.load(ui_file, self)
            ui_file.close()
            
            layout = QVBoxLayout(self)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.addWidget(ui)
            return ui
        raise FileNotFoundError(f"Cannot open: {ui_path}")
    
    def _init_ui(self):
        pass  # Override: load .ui file
    
    def _connect_signals(self):
        pass  # Override: connect handlers
    
    def set_controller(self, controller):
        self._controller = controller
    
    def refresh(self):
        pass  # Override: update from model

基础 Controller 模式

from PySide6.QtCore import QObject

class BaseController(QObject):
    def __init__(self, signals, parent=None):
        super().__init__(parent)
        self._signals = signals
        self._views = []
    
    def register_view(self, view):
        if view not in self._views:
            self._views.append(view)
            view.set_controller(self)
    
    def notify_views(self):
        for view in self._views:
            view.refresh()
    
    def initialize(self):
        pass  # Override: setup logic
    
    def cleanup(self):
        self._views.clear()

集中式信号注册表

from PySide6.QtCore import QObject, Signal

class SignalRegistry(QObject):
    # Domain signals
    job_changed = Signal(str)         # job_id
    job_created = Signal(str)         # job_id
    settings_changed = Signal(str, object)  # key, value
    
    # Connection signals
    broker_connected = Signal(bool)   # connected
    
    # UI signals
    error_occurred = Signal(str, str)  # title, message
    app_closing = Signal()

# Singleton
_registry = None

def get_signal_registry():
    global _registry
    if _registry is None:
        _registry = SignalRegistry()
    return _registry

应用启动(DI 容器)

import sys
from PySide6.QtWidgets import QApplication
from my_app.utils.signals import get_signal_registry

class MyApplication:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        self._qt_app = None
        self._services = {}
        self._controllers = {}
        self._signals = get_signal_registry()
    
    def _register_services(self):
        self._services["db"] = DatabaseService()
    
    def _register_controllers(self):
        self._controllers["job"] = JobController(
            signals=self._signals,
            db=self._services["db"],
        )
        for c in self._controllers.values():
            c.initialize()
    
    def run(self) -> int:
        self._qt_app = QApplication(sys.argv)
        self._register_services()
        self._register_controllers()
        
        window = MainWindow(self._signals, self._controllers)
        window.show()
        return self._qt_app.exec()

Widget 命名约定(.ui 文件)

Widget 类型模式示例
标签*_labeljob_number_label
按钮*_btnsave_btn
单行编辑框*_inputcustomer_input
列表*_listjobs_list
表格*_tablepieces_table
下拉框*_combostatus_combo

信号流

User Action (View)
       ↓
    View emits signal
       ↓
    Controller handles action
       ↓
    Service performs operation
       ↓
    SignalRegistry.emit()
       ↓
    Views call refresh()

最佳实践

1. 切勿通过编程方式创建 UI

❌ 错误:

layout = QVBoxLayout()
btn = QPushButton("Click")
layout.addWidget(btn)

✅ 正确:

self._ui = self._load_ui("my_widget.ui")
self._btn = self._ui.findChild(QPushButton, "action_btn")
2. Controller 切勿操作 UI

❌ 错误:

def activate_job(self):
    self._view.label.setText(job.name)  # NO!

✅ 正确:

def activate_job(self):
    self._signals.job_changed.emit(job_id)  # Views subscribe
3. View 订阅信号
def _connect_signals(self):
    self._signals.job_changed.connect(self._on_job_changed)

def _on_job_changed(self, job_id):
    self.refresh()
4. 提供备用 UI
def _init_ui(self):
    try:
        self._ui = self._load_ui("widget.ui")
    except FileNotFoundError:
        self._create_fallback_ui()
5. 使用服务执行外部操作

Controller 将任务委托给服务:

  • 数据库查询 → Repository 服务
  • 文件操作 → 文件服务
  • 网络调用 → API 服务
  • IPC → Broker 客户端

常用导入

# Qt
from PySide6.QtWidgets import QWidget, QMainWindow
from PySide6.QtCore import Signal, Slot, QFile
from PySide6.QtUiTools import QUiLoader

# Project
from my_app.utils.signals import get_signal_registry
from my_app.controllers.base import BaseController
from my_app.views.base import BaseView
from my_app.models.base import BaseModel

参考资料

评论

支持 Markdown