pyqt6-ui-development-rules by oimiragieo/agent-studio
npx skills add https://github.com/oimiragieo/agent-studio --skill pyqt6-ui-development-rules本技能强制执行构建生产级 PyQt6 桌面应用程序的规则。核心原则包括:通过信号/槽实现严格的 MVC 分离、永不阻塞 UI 线程、通过 QSS 集中管理主题,以及由布局管理器驱动的响应式设计。这些规则可防止最常见的 PyQt6 故障:界面冻结、不可测试的耦合以及特定于平台的渲染错误。
| 反模式 | 失败原因 | 正确方法 |
|---|---|---|
| 直接从 UI 槽调用业务逻辑 | 将 UI 与逻辑耦合;使测试变得不可能并破坏 MVC 架构 | 从 UI 发射信号;通过槽连接到控制器/服务方法 |
| 在主线程上运行网络或文件 I/O 操作 | 阻塞 Qt 事件循环;UI 冻结直到操作完成 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 使用 QThread、QRunnable 或结合 qasync 的 asyncio 进行后台操作 |
| 硬编码像素大小和位置 | 在高 DPI 显示器和不同操作系统 DPI 缩放设置下失效 | 使用布局管理器和大小策略;使用 logicalDpiX() 进行 DPI 感知的大小设置 |
| 在单个控件上设置内联样式 | 造成视觉不一致;极难进行主题化或维护 | 在 QApplication 级别定义单个 QSS 样式表,并使用对象名称/类 |
| 忽略跨平台渲染差异 | 控件大小、字体和边距在 Windows/macOS/Linux 之间存在显著差异 | 在所有目标平台上进行测试;在渲染出现差异的地方使用平台条件逻辑 |
# model.py -- 业务逻辑,无 Qt 依赖
class DataModel:
def __init__(self):
self._items = []
def add_item(self, item: str) -> bool:
if item and item not in self._items:
self._items.append(item)
return True
return False
# controller.py -- 在模型和视图之间进行协调
from PyQt6.QtCore import QObject, pyqtSignal
class Controller(QObject):
items_changed = pyqtSignal(list)
error_occurred = pyqtSignal(str)
def __init__(self, model: DataModel):
super().__init__()
self._model = model
def add_item(self, item: str) -> None:
if self._model.add_item(item):
self.items_changed.emit(self._model._items.copy())
else:
self.error_occurred.emit(f"Could not add: {item}")
# view.py -- 仅 UI,通过信号/槽连接
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QLineEdit, QPushButton, QListWidget
class MainView(QMainWindow):
def __init__(self, controller: Controller):
super().__init__()
self._controller = controller
# 将信号连接到槽
self._controller.items_changed.connect(self._on_items_changed)
self._controller.error_occurred.connect(self._on_error)
# UI 发射信号到控制器 -- 从不直接调用模型
self._add_btn.clicked.connect(lambda: self._controller.add_item(self._input.text()))
def _on_items_changed(self, items: list) -> None:
self._list.clear()
self._list.addItems(items)
from PyQt6.QtCore import QThread, pyqtSignal
class WorkerThread(QThread):
progress = pyqtSignal(int)
finished_with_result = pyqtSignal(object)
error = pyqtSignal(str)
def __init__(self, task_fn, parent=None):
super().__init__(parent)
self._task_fn = task_fn
def run(self):
try:
result = self._task_fn(self.progress.emit)
self.finished_with_result.emit(result)
except Exception as e:
self.error.emit(str(e))
# 在 QApplication 级别应用
app = QApplication(sys.argv)
app.setStyleSheet(Path("styles/dark-theme.qss").read_text())
# QSS 文件
"""
QMainWindow {
background-color: #2b2b2b;
color: #e0e0e0;
}
QPushButton {
background-color: #3c3f41;
border: 1px solid #555;
border-radius: 4px;
padding: 6px 16px;
color: #e0e0e0;
}
QPushButton:hover {
background-color: #4c5052;
}
"""
# 使用布局管理器 -- 永不使用 setGeometry() 或 move()
layout = QVBoxLayout()
layout.addWidget(self._toolbar)
layout.addWidget(self._content, stretch=1) # stretch 填充可用空间
layout.addWidget(self._status_bar)
# 用于响应式网格
grid = QGridLayout()
grid.addWidget(label, 0, 0)
grid.addWidget(input_field, 0, 1)
grid.setColumnStretch(1, 1) # 输入框拉伸,标签保持固定
| 技能 | 关联关系 |
|---|---|
modern-python | 使用 uv、ruff、ty、pytest 进行项目设置 |
python-backend-expert | 用于桌面应用后端的后端服务模式 |
tdd | 用于 Qt 控件测试的测试驱动开发 |
accessibility | 适用于桌面应用程序的无障碍访问审计模式 |
开始前:
阅读 .claude/context/memory/learnings.md 以了解先前的 PyQt6 模式和特定于平台的解决方案。
完成后: 将任何特定于平台的渲染问题、信号/槽模式或 QThread 的注意事项记录到 .claude/context/memory/learnings.md。
假设中断:您的上下文可能会重置。如果它不在记忆中,那就没有发生过。
每周安装数
252
代码仓库
GitHub 星标数
17
首次出现
Jan 27, 2026
安全审计
安装于
codex230
opencode230
gemini-cli227
github-copilot225
cursor215
amp210
This skill enforces rules for building production-quality PyQt6 desktop applications. The core principles are: strict MVC separation via signals/slots, never blocking the UI thread, centralized theming via QSS, and layout-manager-driven responsive design. These rules prevent the most common PyQt6 failures: frozen UIs, untestable coupling, and platform-specific rendering bugs.
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Calling business logic directly from UI slots | Couples UI to logic; makes testing impossible and breaks MVC architecture | Emit signals from UI; connect to controller/service methods via slot |
| Running network or file I/O on the main thread | Blocks the Qt event loop; UI freezes until operation completes | Use QThread, QRunnable, or asyncio with qasync for background operations |
| Hardcoding pixel sizes and positions | Breaks on high-DPI displays and different OS DPI scaling settings | Use layout managers and size policies; use logicalDpiX() for DPI-aware sizing |
| Setting styles inline on individual widgets | Creates visual inconsistency; extremely difficult to theme or maintain | Define a single QSS stylesheet at QApplication level and use object names/classes |
| Ignoring cross-platform rendering differences | Widget sizes, fonts, and margins differ significantly between Windows/macOS/Linux | Test on all target platforms; use platform-conditional logic where rendering diverges |
# model.py -- Business logic, no Qt dependencies
class DataModel:
def __init__(self):
self._items = []
def add_item(self, item: str) -> bool:
if item and item not in self._items:
self._items.append(item)
return True
return False
# controller.py -- Mediates between Model and View
from PyQt6.QtCore import QObject, pyqtSignal
class Controller(QObject):
items_changed = pyqtSignal(list)
error_occurred = pyqtSignal(str)
def __init__(self, model: DataModel):
super().__init__()
self._model = model
def add_item(self, item: str) -> None:
if self._model.add_item(item):
self.items_changed.emit(self._model._items.copy())
else:
self.error_occurred.emit(f"Could not add: {item}")
# view.py -- UI only, connects via signals/slots
from PyQt6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QLineEdit, QPushButton, QListWidget
class MainView(QMainWindow):
def __init__(self, controller: Controller):
super().__init__()
self._controller = controller
# Wire signals to slots
self._controller.items_changed.connect(self._on_items_changed)
self._controller.error_occurred.connect(self._on_error)
# UI emits to controller -- never calls model directly
self._add_btn.clicked.connect(lambda: self._controller.add_item(self._input.text()))
def _on_items_changed(self, items: list) -> None:
self._list.clear()
self._list.addItems(items)
from PyQt6.QtCore import QThread, pyqtSignal
class WorkerThread(QThread):
progress = pyqtSignal(int)
finished_with_result = pyqtSignal(object)
error = pyqtSignal(str)
def __init__(self, task_fn, parent=None):
super().__init__(parent)
self._task_fn = task_fn
def run(self):
try:
result = self._task_fn(self.progress.emit)
self.finished_with_result.emit(result)
except Exception as e:
self.error.emit(str(e))
# Apply at QApplication level
app = QApplication(sys.argv)
app.setStyleSheet(Path("styles/dark-theme.qss").read_text())
# QSS file
"""
QMainWindow {
background-color: #2b2b2b;
color: #e0e0e0;
}
QPushButton {
background-color: #3c3f41;
border: 1px solid #555;
border-radius: 4px;
padding: 6px 16px;
color: #e0e0e0;
}
QPushButton:hover {
background-color: #4c5052;
}
"""
# Use layout managers -- never setGeometry() or move()
layout = QVBoxLayout()
layout.addWidget(self._toolbar)
layout.addWidget(self._content, stretch=1) # stretch fills available space
layout.addWidget(self._status_bar)
# For responsive grids
grid = QGridLayout()
grid.addWidget(label, 0, 0)
grid.addWidget(input_field, 0, 1)
grid.setColumnStretch(1, 1) # input stretches, label stays fixed
| Skill | Relationship |
|---|---|
modern-python | Project setup with uv, ruff, ty, pytest |
python-backend-expert | Backend service patterns for desktop app backends |
tdd | Test-driven development for Qt widget testing |
accessibility | Accessibility audit patterns applicable to desktop apps |
Before starting:
Read .claude/context/memory/learnings.md for prior PyQt6 patterns and platform-specific workarounds.
After completing: Record any platform-specific rendering issues, signal/slot patterns, or QThread gotchas to .claude/context/memory/learnings.md.
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.
Weekly Installs
252
Repository
GitHub Stars
17
First Seen
Jan 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex230
opencode230
gemini-cli227
github-copilot225
cursor215
amp210
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装