pyside6-mvc

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PySide6 MVC Architecture Instructions

PySide6 MVC架构开发指南

Guidelines for building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by
.ui
files.
本指南介绍如何使用PySide6构建采用严格MVC架构且所有UI均由
.ui
文件定义的Python桌面应用。

Architecture Overview

架构概述

┌─────────────────────────────────────────┐
│           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       │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│           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       │
└─────────────────────────────────────────┘

Project Structure

项目结构

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

Core Principles

核心原则

ComponentResponsibilityDoes NOT
ModelData, validation, serializationTouch UI, call services
ViewLoad .ui files, capture inputContain business logic
ControllerCoordinate models & servicesManipulate UI directly
组件职责禁止操作
模型数据、验证、序列化操作UI、调用服务
视图加载.ui文件、捕获输入包含业务逻辑
控制器协调模型与服务直接操作UI

Base Model Pattern

基础模型模式

python
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
python
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

Base View Pattern

基础视图模式

python
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
python
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

Base Controller Pattern

基础控制器模式

python
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()
python
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()

Central Signal Registry

中央信号注册表

python
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()
python
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

Singleton

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

Application Bootstrap (DI Container)

应用启动(依赖注入容器)

python
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()
python
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 Naming Conventions (.ui files)

组件命名规范(.ui文件)

Widget TypePatternExample
Label
*_label
job_number_label
Button
*_btn
save_btn
Line Edit
*_input
customer_input
List
*_list
jobs_list
Table
*_table
pieces_table
Combo
*_combo
status_combo
组件类型命名规则示例
标签
*_label
job_number_label
按钮
*_btn
save_btn
单行输入框
*_input
customer_input
列表
*_list
jobs_list
表格
*_table
pieces_table
下拉框
*_combo
status_combo

Signal Flow

信号流转

User Action (View)
    View emits signal
    Controller handles action
    Service performs operation
    SignalRegistry.emit()
    Views call refresh()
用户操作(视图)
    视图发送信号
    控制器处理操作
    服务执行操作
    SignalRegistry发送信号
    视图调用refresh()

Best Practices

最佳实践

1. Never Create UI Programmatically

1. 切勿通过代码创建UI

❌ Wrong:
python
layout = QVBoxLayout()
btn = QPushButton("Click")
layout.addWidget(btn)
✅ Correct:
python
self._ui = self._load_ui("my_widget.ui")
self._btn = self._ui.findChild(QPushButton, "action_btn")
❌ 错误示例:
python
layout = QVBoxLayout()
btn = QPushButton("Click")
layout.addWidget(btn)
✅ 正确示例:
python
self._ui = self._load_ui("my_widget.ui")
self._btn = self._ui.findChild(QPushButton, "action_btn")

2. Controllers Never Touch UI

2. 控制器切勿直接操作UI

❌ Wrong:
python
def activate_job(self):
    self._view.label.setText(job.name)  # NO!
✅ Correct:
python
def activate_job(self):
    self._signals.job_changed.emit(job_id)  # Views subscribe
❌ 错误示例:
python
def activate_job(self):
    self._view.label.setText(job.name)  # 禁止!
✅ 正确示例:
python
def activate_job(self):
    self._signals.job_changed.emit(job_id)  # 视图订阅该信号

3. Views Subscribe to Signals

3. 视图订阅信号

python
def _connect_signals(self):
    self._signals.job_changed.connect(self._on_job_changed)

def _on_job_changed(self, job_id):
    self.refresh()
python
def _connect_signals(self):
    self._signals.job_changed.connect(self._on_job_changed)

def _on_job_changed(self, job_id):
    self.refresh()

4. Provide Fallback UI

4. 提供备选UI

python
def _init_ui(self):
    try:
        self._ui = self._load_ui("widget.ui")
    except FileNotFoundError:
        self._create_fallback_ui()
python
def _init_ui(self):
    try:
        self._ui = self._load_ui("widget.ui")
    except FileNotFoundError:
        self._create_fallback_ui()

5. Use Services for External Operations

5. 使用服务处理外部操作

Controllers delegate to services:
  • Database queries → Repository services
  • File operations → File service
  • Network calls → API service
  • IPC → Broker client
控制器将任务委托给服务:
  • 数据库查询 → 仓储服务
  • 文件操作 → 文件服务
  • 网络请求 → API服务
  • 进程间通信 → 代理客户端

Common Imports

常用导入

python
undefined
python
undefined

Qt

Qt

from PySide6.QtWidgets import QWidget, QMainWindow from PySide6.QtCore import Signal, Slot, QFile from PySide6.QtUiTools import QUiLoader
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
undefined
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
undefined

References

参考资料