pyside6-mvc
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePySide6 MVC Architecture Instructions
PySide6 MVC架构开发指南
Guidelines for building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by files.
.ui本指南介绍如何使用PySide6构建采用严格MVC架构且所有UI均由文件定义的Python桌面应用。
.uiArchitecture 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 registrymy_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 registryCore Principles
核心原则
| Component | Responsibility | Does NOT |
|---|---|---|
| Model | Data, validation, serialization | Touch UI, call services |
| View | Load .ui files, capture input | Contain business logic |
| Controller | Coordinate models & services | Manipulate 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 NotImplementedErrorpython
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 NotImplementedErrorBase 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 modelpython
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 modelBase 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
undefinedApplication 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 Type | Pattern | Example |
|---|---|---|
| Label | | |
| Button | | |
| Line Edit | | |
| List | | |
| Table | | |
| 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
undefinedpython
undefinedQt
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
undefinedfrom 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