pyside6-mvc
PySide6 MVC Architecture Instructions
Guidelines for building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by .ui files.
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 │
└─────────────────────────────────────────┘
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
Core 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 |
Base Model Pattern
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
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
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
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
Application Bootstrap (DI Container)
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)
| Widget Type | Pattern | Example |
|---|---|---|
| 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 |
Signal Flow
User Action (View)
↓
View emits signal
↓
Controller handles action
↓
Service performs operation
↓
SignalRegistry.emit()
↓
Views call refresh()
Best Practices
1. Never Create UI Programmatically
❌ 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")
2. Controllers Never Touch UI
❌ 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
3. Views Subscribe to Signals
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
def _init_ui(self):
try:
self._ui = self._load_ui("widget.ui")
except FileNotFoundError:
self._create_fallback_ui()
5. Use Services for External Operations
Controllers delegate to services:
- Database queries → Repository services
- File operations → File service
- Network calls → API service
- IPC → Broker client
Common Imports
# 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
References
More from ds-codi/project-memory-mcp
pyside6-qml-views
Use this skill when creating QML view files, designing QML component hierarchies, building layouts, styling QML controls, creating reusable QML components, implementing QML navigation / page switching, or working with QML resources. Covers QML file structure, component patterns, Material/Controls styling, resource management, and common QML idioms for desktop applications.
49pyside6-qml-architecture
Use this skill when creating a new PySide6 + QML desktop application with MVC architecture, setting up project structure, implementing the application bootstrap / DI container, or understanding how the MVC layers connect. Covers project scaffolding, entry points, singleton application class, service locator, signal registry, and lifecycle management.
47mvc-architecture
Use this skill when implementing Model-View-Controller architecture. Covers core MVC principles, layer separation, dependency injection, event-driven communication, and patterns for controllers, models, and views.
40pyside6-qml-models-services
Use this skill when creating domain models with Qt signal support, implementing the repository pattern for data persistence, building service classes for external interactions, designing the central signal registry, or working with application state management. Covers BaseModel, model serialization, database repositories, service patterns, signal definitions, and the ApplicationState singleton.
34pyside6-qml-bridge
Use this skill when exposing Python objects to QML, creating bridge classes, defining Qt properties with NOTIFY signals, implementing invokable methods / slots, or connecting QML user actions to Python controllers. Covers the QObject bridge pattern, property decorators, type conversions, context properties, and QML type registration.
32bugfix
One-command automatic bug-fix orchestrator for this Project Memory MCP workspace using Revisionist → Executor → Reviewer loops plus testing.
29