pyside6-qml-architecture
PySide6 QML MVC Architecture
Desktop GUI applications in this workspace use Python + PySide6 with QML files for the view layer, following a strict Model-View-Controller (MVC) architecture. This skill documents the canonical project structure, bootstrap pattern, and layer responsibilities derived from the ds_pas/ application.
Architecture Overview
┌─────────────────────────────────────────┐
│ View Layer (QML files) │
│ Declarative UI, data binding, signals │
└──────────────────┬──────────────────────┘
│ Properties, Signals, Slots
┌──────────────────▼──────────────────────┐
│ Python-QML Bridge │
│ QObject subclasses exposed to QML │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ Controller Layer │
│ Coordinate models & services │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ Model Layer │
│ Data structures, validation, signals │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ Services Layer │
│ Database, files, network, broker │
└─────────────────────────────────────────┘
Project Structure
my_app/
├── app.py # Bootstrap & DI container (singleton)
├── __init__.py # Package init
├── __main__.py # Entry point: python -m my_app
├── controllers/
│ ├── __init__.py
│ ├── base.py # BaseController with view registry
│ └── *_controller.py # Domain controllers (job, settings, etc.)
├── models/
│ ├── __init__.py
│ ├── base.py # BaseModel with property_changed signal
│ ├── state.py # ApplicationState singleton
│ └── *.py # Domain models (job, piece, customer, etc.)
├── views/
│ ├── __init__.py
│ ├── bridge.py # QObject bridge classes exposed to QML
│ ├── main_window.py # Main window setup (QQmlApplicationEngine)
│ └── components/ # Reusable Python view helpers
├── services/
│ ├── __init__.py
│ ├── database/ # Repository pattern for data access
│ └── *.py # External interaction services
├── resources/
│ ├── qml/
│ │ ├── main.qml # Root QML component
│ │ ├── components/ # Reusable QML components
│ │ ├── pages/ # Page-level QML views
│ │ └── styles/ # Theme and style definitions
│ ├── icons/ # SVG/PNG icons
│ └── qml.qrc # Qt resource file (optional)
├── utils/
│ ├── __init__.py
│ ├── signals.py # Central SignalRegistry
│ ├── types.py # Type aliases, protocols, ServiceLocator
│ └── paths.py # Path resolution helpers
└── tests/
└── *.py
Layer Responsibilities
| Component | Responsibility | MUST NOT |
|---|---|---|
| Model | Data structures, validation, serialization, property_changed signals |
Touch UI, call services, reference QML |
| View (QML) | Declarative UI layout, data binding to bridge properties, user input capture | Contain business logic, call services directly |
| Bridge | QObject subclasses that expose model data and controller actions to QML | Contain business logic, directly manipulate QML |
| Controller | Coordinate models & services, handle actions, emit signals | Manipulate UI directly, import QML types |
| Service | Database queries, file I/O, network calls, IPC | Reference models, views, or controllers |
Application Bootstrap (DI Container)
The application class is a singleton that wires all layers together:
"""app.py — Bootstrap & DI container."""
import sys
import logging
from typing import Any
from pathlib import Path
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QObject, QUrl
from my_app.utils.signals import SignalRegistry, get_signal_registry
from my_app.utils.types import ServiceLocator
logger = logging.getLogger(__name__)
class MyApplication:
"""Singleton application with DI container."""
_instance: "MyApplication | None" = None
def __new__(cls, dev_mode: bool = False) -> "MyApplication":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, dev_mode: bool = False) -> None:
if self._initialized:
return
self._initialized = True
self._dev_mode = dev_mode
self._qt_app: QApplication | None = None
self._engine: QQmlApplicationEngine | None = None
self._services: dict[str, Any] = {}
self._controllers: dict[str, Any] = {}
self._signals = get_signal_registry()
self._service_locator = ServiceLocator()
def _register_services(self) -> None:
"""Create and register all service instances."""
# self._services["db"] = DatabaseService(...)
pass
def _register_controllers(self) -> None:
"""Create controllers with injected dependencies."""
# self._controllers["job"] = JobController(
# signals=self._signals,
# repository=self._services["db"],
# )
pass
def _register_qml_types(self) -> None:
"""Register Python bridge objects as QML context properties."""
ctx = self._engine.rootContext()
# ctx.setContextProperty("jobBridge", self._bridges["job"])
pass
def run(self) -> int:
self._qt_app = QApplication(sys.argv)
self._register_services()
self._register_controllers()
self._engine = QQmlApplicationEngine()
self._register_qml_types()
qml_path = Path(__file__).parent / "resources" / "qml" / "main.qml"
self._engine.load(QUrl.fromLocalFile(str(qml_path)))
if not self._engine.rootObjects():
return -1
return self._qt_app.exec()
Entry Point
"""__main__.py"""
import sys
from my_app.app import MyApplication
def main():
app = MyApplication(dev_mode="--dev" in sys.argv)
sys.exit(app.run())
if __name__ == "__main__":
main()
ServiceLocator Pattern
"""utils/types.py — Type aliases, protocols, and DI container."""
from typing import Any, Protocol, TypeVar, runtime_checkable
T = TypeVar("T")
@runtime_checkable
class IController(Protocol):
def initialize(self) -> None: ...
def cleanup(self) -> None: ...
@runtime_checkable
class IService(Protocol):
def initialize(self) -> None: ...
def shutdown(self) -> None: ...
class ServiceLocator:
"""Lightweight DI container for service instances."""
_instance: "ServiceLocator | None" = None
def __new__(cls) -> "ServiceLocator":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._services = {}
return cls._instance
def register(self, name: str, service: Any) -> None:
self._services[name] = service
def get(self, name: str) -> Any:
if name not in self._services:
raise KeyError(f"Service '{name}' not registered")
return self._services[name]
def has(self, name: str) -> bool:
return name in self._services
Signal Flow
User Action (QML)
↓
QML emits signal / calls slot on Bridge
↓
Bridge delegates to Controller
↓
Controller calls Service
↓
Service returns result
↓
Controller updates Model
↓
Model emits property_changed
↓
Bridge property notifies QML (via NOTIFY)
↓
QML binding automatically updates UI
Key Design Rules
- QML files are declarative only — no JavaScript business logic, no direct service calls
- Python bridge objects are the sole interface between QML and the Python backend
- Controllers never import QML types — they operate through bridge signals/properties
- Models are pure data — no UI imports, no service calls
- Services are stateless workers — no model references, no UI knowledge
- All cross-layer communication flows through the
SignalRegistryor Qt property bindings - Singletons (
Application,SignalRegistry,ServiceLocator,ApplicationState) use the__new__pattern for thread-safe reuse
File Size Guidelines
- Keep files focused on a single responsibility
- Split files exceeding ~300-400 lines into submodules
- Extract reusable QML components into
resources/qml/components/ - Group related controllers/services by domain (e.g.,
controllers/job_controller.py)
References
More from ds-codi/project-memory-mcp
pyside6-mvc
Use this skill when building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by .ui files. Covers architecture patterns, controller/model/view separation, signal handling, and .ui file workflows.
95pyside6-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.
49mvc-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