关于 pyqt
用于 PyQt 和 PySide 桌面应用开发的技能导航中心,按主题提供 UI 组件、线程、测试、样式等指南。
主要功能
- 01涵盖七个 Qt 专项主题
- 02提供 MVC 与 MVVM 架构建议
- 03比较 PyQt 和 PySide
- 04介绍 PyInstaller 与 cx_Freeze 打包
- 05提供故障排查和开发实践
适用场景
- 01选择 Qt 界面架构
- 02查找信号、控件或对话框指南
- 03将耗时任务移出 UI 线程
- 04测试或打包 Qt 应用
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codeatcode/ oss-ai-skills --skill pyqt安装后在 Claude Code 中输入/pyqt确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
multimedia/references/codecs-platforms.mdmedia-pipeline.mdstyling/references/dark-theme.mdthreading/references/advanced-safety.mdlifecycle-cleanup.mdpool-patterns.mdqt-concurrent.mdtesting.mdwidgets/references/event-handling.mditem-views-layouts.md详细介绍
PyQt/PySide 开发
PyQt 和 PySide 是 Qt 应用框架的 Python 绑定,可用于构建跨平台桌面应用程序。
子技能
如需详细信息,请参阅以下专项子技能:
| 技能 | 说明 | 路径 |
|---|---|---|
| pyqt-core | 信号、槽、定时器、设置、文件 I/O | core/SKILL.md |
| pyqt-widgets | 所有组件和布局 | widgets/SKILL.md |
| pyqt-threading | QThread、线程池、并发 | threading/SKILL.md |
| pyqt-dialogs | 标准对话框和自定义对话框 | dialogs/SKILL.md |
| pyqt-testing | pytest-qt 测试模式 | testing/SKILL.md |
| pyqt-styling | QSS 样式和主题 | styling/SKILL.md |
| pyqt-multimedia | 音频、视频、摄像头、录制 | multimedia/SKILL.md |
架构决策:Qt 中的 MVC 与 MVVM
Qt 支持多种架构模式。请根据数据展示的复杂程度进行选择。
MVC(模型-视图-控制器)
适用场景:简单的数据展示和内联编辑、标准项视图。
- 模型:
QAbstractItemModel子类拥有数据 - 视图:
QTableView、QTreeView、QListView展示数据 - 控制器:内置于视图中(表头点击、选择处理)
- 验证:在模型的
setData()方法中进行 - 信号:模型发出
dataChanged()、rowsInserted(),通知视图
# Model owns data and validation
class DataModel(QAbstractTableModel):
def setData(self, index, value, role):
if not self._validate(value):
return False
# update and emit dataChanged
MVVM(模型-视图-视图模型)
适用场景:同一数据以不同格式显示在多个组件中、复杂的 UI 状态。
- 模型:数据源(数据库、API、文件)
- 视图模型:转换模型数据以供展示,并拥有 UI 状态
- 视图:通过信号与视图模型绑定的 PyQt 组件
- 验证:在视图模型中更新模型之前进行
- 信号:视图模型公开
Property信号;视图与其连接
# ViewModel transforms data for display
class UserViewModel(QObject):
display_name = Property(str, _display_name_changed)
def __init__(self, user_model):
self._user = user_model
self._user.nameChanged.connect(self._on_model_changed)
实用测试:格式化逻辑应该放在哪里?
如果同一数据以不同格式显示在两个组件中,格式化逻辑应放在视图模型或委托中,而不是在两个组件中重复实现。
不佳:
# Widget A
label.setText(f"{value:.2f} €")
# Widget B
label.setText(f"€ {value:,.2f}")
良好:
# ViewModel provides formatted strings
class PriceViewModel(QObject):
display_eu = Property(str)
display_us = Property(str)
def set_price(self, value):
self._price = value
self.display_eu_changed.emit()
self.display_us_changed.emit()
信号/槽:什么时候仅靠它们就够了?
满足以下情况时,仅使用信号和槽就足够:
- 一个组件展示一个数据源
- 没有复杂的派生状态(例如,“仅当 Y 和 Z 满足条件时才显示 X”)
- 验证逻辑可以放在模型的
setData()中 - 无需在不创建组件的情况下测试 UI 逻辑
满足以下情况时,请添加视图模型层:
- 多个视图需要以不同格式展示相同数据
- UI 状态(启用/禁用、可见性)取决于复杂条件
- 希望不实例化组件也能测试展示逻辑
这应该放在哪里?
使用以下路由,只加载你需要的子技能:
| 意图 | 加载此子技能 |
|---|---|
信号声明、槽装饰器、pyqtSignal/Signal、类型化信号、connect()、disconnect()、信号链 | pyqt/core |
| 组件组合(使用多个组件构建自定义组件)、项视图(QTableView、QTreeView)、委托(QItemDelegate)、事件过滤器 | pyqt/widgets |
QThread 工作对象模式、QThreadPool/QRunnable、QExecutor、线程安全、取消、阻塞操作 | pyqt/threading |
| 标准对话框(QFileDialog、QMessageBox、QInputDialog、QColorDialog、QFontDialog)、自定义 QDialog 模式、模态与非模态 | pyqt/dialogs |
pytest-qt fixture(qtbot)、waitSignal、鼠标/键盘模拟、对话框测试、模型/视图测试 | pyqt/testing |
QSS 语法、伪状态(:hover、:pressed)、组件专用样式、主题、平台差异 | pyqt/styling |
Hub 中不应包含的内容
- 信号/槽基础知识 →
pyqt/core(此 Hub 仅负责路由) - 控件列表 →
pyqt/widgets(此 Hub 仅负责路由) - QSS 语法和示例 →
pyqt/styling(此 Hub 仅负责路由) - 布局代码片段 →
pyqt/widgets(此 Hub 仅负责路由)
深入了解
针对专门主题加载以下子技能:
- 深入了解信号/槽 →
pyqt/core- 信号声明、槽装饰器、连接、类型化信号 - 控件、布局、项目视图、事件处理 →
pyqt/widgets- 显示/输入/容器控件、QVBoxLayout/QHBoxLayout/QGridLayout、事件筛选器、快捷键 - QSS 样式和深色主题 →
pyqt/styling- QSS 语法、伪状态、特定控件样式、基于属性的样式 - 对话框 →
pyqt/dialogs- 标准对话框(QFileDialog、QMessageBox、QInputDialog)、自定义 QDialog 模式、模态/非模态 - 线程模式 →
pyqt/threading- QThread 工作对象模式、QThreadPool/QRunnable、线程安全、取消 - pytest-qt 测试 →
pyqt/testing- qtbot fixture、waitSignal、鼠标/键盘模拟、对话框测试、模型/视图测试
PyQt 与 PySide 对比
| 特性 | PyQt5 | PyQt6 | PySide6 |
|---|---|---|---|
| 许可证 | GPL | GPL | LGPL |
| Qt 版本 | Qt 5 | Qt 6 | Qt 6 |
| 维护状态 | 仅安全维护 | 积极维护 | 积极维护 |
| 信号语法 | pyqtSignal | pyqtSignal | Signal |
| 槽语法 | pyqtSlot | pyqtSlot | Slot |
| 属性语法 | pyqtProperty | pyqtProperty | Property |
| 商业用途 | 需要许可证 | 需要许可证 | 免费 |
| QML 注册 | qmlRegisterType() | qmlRegisterType() | @QmlElement |
如何选择
- PySide6:推荐用于大多数项目(LGPL,Qt Company 官方支持)
- PyQt6:需要兼容 GPL 或已有 PyQt 代码库时使用
- PyQt5:仅用于遗留项目(仅提供安全修复)
安装
PySide6(推荐)
pip install PySide6
PyQt6
pip install PyQt6
PyQt5(遗留版本)
pip install PyQt5
其他依赖项
# System packages (Ubuntu/Debian)
sudo apt install libgl1-mesa-glx libglib2.0-0
# System packages (Fedora)
sudo dnf install mesa-libGL glib2
# System packages (Arch)
sudo pacman -S mesa glib2
基础应用程序
#!/usr/bin/env python3
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel
from PySide6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My Application")
self.setGeometry(100, 100, 800, 600)
label = QLabel("Hello, Qt!")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setCentralWidget(label)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
推荐的项目结构
my_app/
├── src/
│ ├── __init__.py
│ ├── main.py
│ ├── main_window.py
│ ├── widgets/
│ │ ├── __init__.py
│ │ └── custom_widget.py
│ ├── models/
│ │ └── data_model.py
│ ├── resources/
│ │ ├── icons/
│ │ └── styles/
│ │ └── style.qss
│ └── utils/
│ └── helpers.py
├── tests/
│ └── test_main.py
├── requirements.txt
└── pyproject.toml
快速参考
核心导入
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox, QSpinBox, QCheckBox, QSlider, QProgressBar, QGroupBox, QTabWidget, QStackedWidget, QSplitter, QListWidget, QTreeWidget, QTableWidget, QScrollArea, QToolBar, QStatusBar
from PySide6.QtCore import Qt, QObject, QTimer, QThread, Signal, Slot, Property, QSize, QPoint, QRect, QSettings, QFile, QDir, QUrl, QMimeData, QDateTime
from PySide6.QtGui import QIcon, QPixmap, QImage, QPainter, QPen, QBrush, QColor, QFont, QCursor, QKeySequence, QShortcut
常用属性
widget.setEnabled(True) # Enable/disable
widget.setVisible(False) # Visibility
widget.setToolTip("Help") # Tooltip
widget.setObjectName("myButton") # QSS selector
widget.setProperty("primary", True) # Custom property
打包与分发
PyInstaller
# Install
pip install pyinstaller
# Single executable
pyinstaller --onefile --windowed app.py
# With icon and data files
pyinstaller --onefile --windowed --icon=app.ico --add-data "resources:resources" app.py
PyInstaller Spec 文件
# app.spec
a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[('resources', 'resources')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
icon='app.ico',
)
cx_Freeze
# setup.py
from cx_Freeze import setup, Executable
build_options = {
'packages': ['PySide6'],
'excludes': ['tkinter'],
'include_files': [('resources', 'resources')]
}
setup(
name='MyApp',
version='1.0',
description='My Qt Application',
options={'build_exe': build_options},
executables=[Executable('app.py', base='Win32GUI', icon='app.ico')]
)
构建命令
# PyInstaller
pyinstaller --clean app.spec
# cx_Freeze
python setup.py build
# Create distribution
pyinstaller --clean --distpath dist app.spec
分发检查清单
- 在干净的 VM 上测试(未安装 Python)
- 确认已打包所有资源(图标、QSS、翻译)
- 检查可执行文件大小(如有需要,剥离调试符号)
- 在目标操作系统版本上测试
- 对可执行文件签名(Windows/macOS)
- 创建安装程序(可选:NSIS、Inno Setup、dmgbuild)
最佳实践
架构
- 将 UI 与业务逻辑分离 - 使用 MVC 或 MVVM 模式
- 使用依赖注入 - 将依赖项传递给构造函数
- 保持控件无状态 - 将状态存储在模型中,而不是控件中
- 使用信号实现解耦 - 组件通过信号通信
- 延迟加载大型资源 - 按需加载图像/数据
性能
- 避免阻塞主线程 - 长时间运行的操作使用工作线程
- 对复杂项目渲染使用视图委托 - 不要不必要地继承 QItemDelegate
- 批量更新 UI - 批量更改期间阻止信号
- 复用控件 - 对频繁创建的控件使用对象池
- 先分析性能再优化 - 使用
python -m cProfile
内存管理
- 使用父子关系 - Qt 会自动清理子对象
- 对动态控件调用
deleteLater()- 不要直接使用del - 断开信号/槽连接 - 删除前先断开连接
- 避免循环引用 - 必要时使用弱引用
- 使用
tracemalloc监控 - 查找内存泄漏
代码组织
- 每个文件放一个类 - 保持模块专注
- 将相关控件分组 - 在单独的模块中定义自定义控件
- 集中管理常量 - 使用枚举表示固定值
- 使用类型提示 - 有助于 IDE 支持和调试
- 为公共 API 编写文档 - 为方法和类添加文档字符串
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| UI 卡顿 | 主线程中执行阻塞操作 | 移至工作线程 |
| 访问控件时崩溃 | 从工作线程访问 UI | 改用信号 |
| 内存泄漏 | 对象未清理 | 使用父子关系,调用 deleteLater() |
| QSS 未生效 | 选择器或语法错误 | 检查控件的 objectName,调用 style().polish() |
| 信号未触发 | 连接类型错误 | 检查信号声明,并确认线程关联 |
| CPU 使用率高 | 主线程中存在紧密循环 | 使用定时器或工作线程 |
| 窗口未显示 | 未调用 show() | 在 app.exec() 前调用 window.show() |
| 图标未加载 | 路径或格式错误 | 使用 QResource 打包资源 |
调试提示
# Enable Qt logging
import logging
logging.basicConfig(level=logging.DEBUG)
# Check for unhandled exceptions
import sys
sys.excepthook = lambda exc: print(f"Unhandled: {exc}")
# Print widget hierarchy
print(window.findChildren(QWidget))
# Check signal connections
print(button.receivers(button.clicked))
# Dump QSS errors
app.setStyleSheet("INVALID {") # Will print parse errors
参考资料
评论
支持 Markdown