mcpField
P
pyside6-qml-architecture

pyside6-qml-architecture

github.com/ds-codiDevelopment47 installs
Repository

About pyside6-qml-architecture

A skill defining MVC architecture, project structure, and layer responsibilities for Python, PySide6, and QML desktop applications. It covers bootstrap and dependency-injection patterns, signal flow, and design constraints.

Key features

  1. 01Define MVC layer responsibilities
  2. 02Specify the Python-QML bridge role
  3. 03Provide bootstrap and dependency-injection patterns
  4. 04Describe signal and property-binding communication
  5. 05Outline project structure and file-splitting guidance

Use cases

  1. 01Plan a PySide6 QML application structure
  2. 02Clarify boundaries among views, controllers, and services
  3. 03Design application startup and cross-layer communication

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/ds-codi/project-memory-mcp --skill pyside6-qml-architecture

After installing, type/pyside6-qml-architecturein Claude Code to confirm it loaded.

Details

From the project README

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

ComponentResponsibilityMUST NOT
ModelData structures, validation, serialization, property_changed signalsTouch UI, call services, reference QML
View (QML)Declarative UI layout, data binding to bridge properties, user input captureContain business logic, call services directly
BridgeQObject subclasses that expose model data and controller actions to QMLContain business logic, directly manipulate QML
ControllerCoordinate models & services, handle actions, emit signalsManipulate UI directly, import QML types
ServiceDatabase queries, file I/O, network calls, IPCReference 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

  1. QML files are declarative only — no JavaScript business logic, no direct service calls
  2. Python bridge objects are the sole interface between QML and the Python backend
  3. Controllers never import QML types — they operate through bridge signals/properties
  4. Models are pure data — no UI imports, no service calls
  5. Services are stateless workers — no model references, no UI knowledge
  6. All cross-layer communication flows through the SignalRegistry or Qt property bindings
  7. 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

Comments

Markdown supported