重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
pyside6-mvc by ds-codi/project-memory-mcp
npx skills add https://github.com/ds-codi/project-memory-mcp --skill pyside6-mvc使用 PySide6 构建 Python 桌面应用程序的指南,采用严格的 MVC 架构,所有 UI 均由 .ui 文件定义。
┌─────────────────────────────────────────┐
│ 视图层 (.ui 文件) │
│ 从 Qt Designer 加载,捕获用户输入 │
└──────────────────┬──────────────────────┘
│ 信号
┌──────────────────▼──────────────────────┐
│ 控制器层 │
│ 协调模型与服务 │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ 模型层 │
│ 数据结构,数据验证 │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ 服务层 │
│ 数据库、文件、网络、消息代理 │
└─────────────────────────────────────────┘
my_app/
├── app.py # 引导程序和依赖注入容器
├── __main__.py # 程序入口点
├── controllers/
│ ├── base.py # BaseController
│ └── *_controller.py # 领域控制器
├── models/
│ ├── base.py # 带信号的 BaseModel
│ └── *.py # 领域模型
├── views/
│ ├── base.py # BaseView
│ └── *.py # 视图类
├── services/
│ └── *.py # 外部交互服务
├── resources/
│ └── ui/ # .ui 文件 (Qt Designer)
└── utils/
└── signals.py # 中央信号注册表
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 组件 | 职责 | 禁止行为 |
|---|---|---|
| 模型 | 数据、验证、序列化 | 接触 UI、调用服务 |
| 视图 | 加载 .ui 文件、捕获输入 | 包含业务逻辑 |
| 控制器 | 协调模型与服务 | 直接操作 UI |
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
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 # 重写:加载 .ui 文件
def _connect_signals(self):
pass # 重写:连接信号处理器
def set_controller(self, controller):
self._controller = controller
def refresh(self):
pass # 重写:根据模型更新视图
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 # 重写:初始化逻辑
def cleanup(self):
self._views.clear()
from PySide6.QtCore import QObject, Signal
class SignalRegistry(QObject):
# 领域信号
job_changed = Signal(str) # job_id
job_created = Signal(str) # job_id
settings_changed = Signal(str, object) # key, value
# 连接信号
broker_connected = Signal(bool) # connected
# UI 信号
error_occurred = Signal(str, str) # title, message
app_closing = Signal()
# 单例
_registry = None
def get_signal_registry():
global _registry
if _registry is None:
_registry = SignalRegistry()
return _registry
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()
| 控件类型 | 模式 | 示例 |
|---|---|---|
| 标签 | *_label | job_number_label |
| 按钮 | *_btn | save_btn |
| 行编辑框 | *_input | customer_input |
| 列表 | *_list | jobs_list |
| 表格 | *_table | pieces_table |
| 组合框 | *_combo | status_combo |
用户操作 (视图)
↓
视图发出信号
↓
控制器处理操作
↓
服务执行操作
↓
SignalRegistry.emit()
↓
视图调用 refresh()
❌ 错误做法:
layout = QVBoxLayout()
btn = QPushButton("Click")
layout.addWidget(btn)
✅ 正确做法:
self._ui = self._load_ui("my_widget.ui")
self._btn = self._ui.findChild(QPushButton, "action_btn")
❌ 错误做法:
def activate_job(self):
self._view.label.setText(job.name) # 禁止!
✅ 正确做法:
def activate_job(self):
self._signals.job_changed.emit(job_id) # 视图订阅此信号
def _connect_signals(self):
self._signals.job_changed.connect(self._on_job_changed)
def _on_job_changed(self, job_id):
self.refresh()
def _init_ui(self):
try:
self._ui = self._load_ui("widget.ui")
except FileNotFoundError:
self._create_fallback_ui()
控制器将任务委托给服务:
# Qt
from PySide6.QtWidgets import QWidget, QMainWindow
from PySide6.QtCore import Signal, Slot, QFile
from PySide6.QtUiTools import QUiLoader
# 项目内部
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
每周安装量
65
代码仓库
GitHub 星标数
4
首次出现
2026年2月27日
安全审计
安装于
codex65
kimi-cli64
amp64
cline64
github-copilot64
gemini-cli64
Guidelines for building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by .ui files.
┌─────────────────────────────────────────┐
│ 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
| 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 |
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
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
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
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 Type | Pattern | Example |
|---|---|---|
| Label | *_label | job_number_label |
| Button | *_btn | save_btn |
| Line Edit | *_input | customer_input |
| List | *_list |
User Action (View)
↓
View emits signal
↓
Controller handles action
↓
Service performs operation
↓
SignalRegistry.emit()
↓
Views call refresh()
❌ Wrong:
layout = QVBoxLayout()
btn = QPushButton("Click")
layout.addWidget(btn)
✅ Correct:
self._ui = self._load_ui("my_widget.ui")
self._btn = self._ui.findChild(QPushButton, "action_btn")
❌ Wrong:
def activate_job(self):
self._view.label.setText(job.name) # NO!
✅ Correct:
def activate_job(self):
self._signals.job_changed.emit(job_id) # Views subscribe
def _connect_signals(self):
self._signals.job_changed.connect(self._on_job_changed)
def _on_job_changed(self, job_id):
self.refresh()
def _init_ui(self):
try:
self._ui = self._load_ui("widget.ui")
except FileNotFoundError:
self._create_fallback_ui()
Controllers delegate to services:
# 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
Weekly Installs
65
Repository
GitHub Stars
4
First Seen
Feb 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex65
kimi-cli64
amp64
cline64
github-copilot64
gemini-cli64
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
166,500 周安装
jobs_list |
| Table | *_table | pieces_table |
| Combo | *_combo | status_combo |