mcpField
P
pyside6-mvc

pyside6-mvc

github.com/ds-codiDevelopment95 installs
Repository

About pyside6-mvc

A skill with architectural guidance for Python desktop applications built with PySide6. It is intended for developers seeking strict MVC separation and UI defined in Qt Designer files.

Key features

  1. 01Strict model-view-controller separation
  2. 02UI loaded from .ui files
  3. 03Signal-based component communication
  4. 04Central signal registry
  5. 05Application bootstrap with dependency injection

Use cases

  1. 01Structuring a PySide6 desktop application
  2. 02Separating UI, business logic, and data
  3. 03Coordinating views through shared signals

Install

Copy the command and run it in a terminal.

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

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

Details

From the project README

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

ComponentResponsibilityDoes NOT
ModelData, validation, serializationTouch UI, call services
ViewLoad .ui files, capture inputContain business logic
ControllerCoordinate models & servicesManipulate 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 TypePatternExample
Label*_labeljob_number_label
Button*_btnsave_btn
Line Edit*_inputcustomer_input
List*_listjobs_list
Table*_tablepieces_table
Combo*_combostatus_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

Comments

Markdown supported