20 KiB
Windows Qt / PySide / PyQt 适配指南
目录
- 确认绑定与 UI 技术
- 将 Windows 交互意图映射到 Qt
- 架构、对象生命周期与数据模型
- 布局、窗口化与高 DPI
- 主题、QSS、图标与 Windows 材质
- 命令、键盘与无障碍
- 线程、异步与响应速度
- Designer、资源、设置与本地化
- 部署与测试
- 常见反模式
- 交付检查清单
- 官方资料
1. 确认绑定与 UI 技术
先检查 pyproject.toml、锁文件、requirements、导入语句和实际安装版本,再生成代码。
选择绑定
- 新项目优先选择 Qt 6 绑定,除非插件、操作系统或现有代码明确要求 Qt 5。
- PySide6 是 Qt 官方 Python 绑定,采用 LGPLv3/GPLv3/商业许可组合;发布前确认应用对 Qt 许可义务的满足方式。
- PyQt6 采用 GPLv3 或 Riverbank 商业许可,不提供 LGPL;闭源分发前必须确认许可方案。
- 维护现有 PySide2/PyQt5 项目时遵循项目版本,不要在 UI 改造中顺带升级主版本。
- 不要在一个进程或分发包中混用 PySide 与 PyQt。第三方组件的绑定必须与应用一致。
选择 UI 技术
| 场景 | 优先选择 |
|---|---|
| 高密度生产力工具、表格、菜单、工具栏、停靠面板 | Qt Widgets |
既有 .ui 文件或大量成熟 QWidget 控件 |
Qt Widgets |
| 高度定制的视觉、连续动画、触控优先界面 | Qt Quick/QML |
| Widgets 与 QML 混合 | 仅在价值明确时采用,并定义边界、线程、焦点和无障碍责任 |
不要把 Qt Quick 仅当作“更现代的 Widgets”,也不要为了原生外观强行用 Widgets 实现复杂动画。根据任务、团队能力、组件生态、性能、部署和无障碍要求选择。
保持绑定 API 一致
- PySide 使用
Signal、Slot、Property;PyQt 使用pyqtSignal、pyqtSlot、pyqtProperty。 - PySide 项目仅在项目已统一启用时使用
snake_case或true_property特性;不要与默认 camelCase 风格随机混用。 - 生成示例时沿用仓库现有导入风格、枚举写法和 UI 构建方式。
2. 将 Windows 交互意图映射到 Qt
根据语义选择控件,不要逐字翻译 WinUI 控件名:
| Windows/WinUI 意图 | Qt Widgets 实现 | 说明 |
|---|---|---|
| 应用主窗口 | QMainWindow + centralWidget |
可组合菜单栏、工具栏、状态栏和停靠面板 |
| 顶级导航 | 带 Model 的 QListView + QStackedWidget |
小型静态同级页也可用 QTabWidget;导航必须有文本标签 |
| 文档标签页 | QTabWidget 或 QTabBar + 内容容器 |
定义关闭、重排、未保存和快捷键行为 |
| 辅助工作面板 | QDockWidget |
定义允许区域、浮动、关闭和状态恢复 |
| 分栏工作区 | QSplitter |
设置合理最小尺寸与折叠行为 |
| 页面级命令 | QAction + QToolBar/按钮 |
同一 QAction 复用于菜单、工具栏和快捷键 |
| 应用菜单 | QMenuBar、QMenu、QAction |
使用标准菜单分组和角色 |
| 上下文命令 | QMenu 或 QAction 集合 |
同时提供可见或键盘访问路径 |
| 列表 | QListView + QAbstractListModel |
动态或大型数据不要依赖 QListWidget |
| 表格 | QTableView + QAbstractTableModel |
排序/筛选使用 QSortFilterProxyModel |
| 树 | QTreeView + QAbstractItemModel |
保持展开、选择和键盘行为 |
| 简单表单 | QFormLayout + 标准输入控件 |
设置标签 Buddy、验证和错误位置 |
| 模态任务 | QDialog + QDialogButtonBox |
仅用于真正需要阻塞的决策或短任务 |
| 简单系统消息 | QMessageBox |
不要用于频繁状态或长文本工作流 |
| 内联信息条 | 自定义语义化 Banner QWidget |
包含图标、文本、操作和关闭入口,不要用状态栏替代关键错误 |
| 确定进度 | QProgressBar |
提供取消、失败和完成状态 |
| 阻塞进度 | QProgressDialog |
仅在确实需要窗口级阻塞时使用 |
| 持久设置 | QSettings |
先设置组织名和应用名,定义迁移与默认值 |
Qt Quick/QML 项目使用 Qt Quick Controls 的等价语义控件,并为 Python 暴露的状态使用 Signal、Slot 和带通知信号的 Property。不要把 QWidget 嵌入 QML 作为常规布局方案。
核心交互模式映射
数据表格: 使用 QTableView、自定义 QAbstractTableModel 与 QSortFilterProxyModel,以稳定记录 ID 维护业务选择和勾选状态。currentIndex()、selectionModel() 与模型中的勾选角色分别对应当前单元格、选择集和勾选集,不能互相替代。通过视图的 clicked、doubleClicked、activated 与 customContextMenuRequested 明确区分单击、双击、键盘调用和右键;右键先解析目标索引,再按产品规则决定是否改变选择。复选框和行内按钮的委托事件必须停止继续触发行默认操作。
表单: 使用标准编辑控件、QFormLayout、标签 Buddy 和字段旁固定错误区。格式约束可用 QValidator 或输入掩码,领域规则仍由应用层验证。避免在每次按键后弹框;提交失败时保留输入,聚焦首个错误,并给出问题与修复方式。异步验证以请求 ID 或取消机制忽略过期结果。
导航与标签: 顶级导航可用 Model 驱动的 QListView 配合 QStackedWidget;文档工作区使用 QTabWidget,或以 QTabBar 加内容容器构建。页面与标签都用稳定 ID 管理,不以标题或当前索引作为业务身份。关闭、重排和切换前定义未保存检查、焦点返回、筛选/选择/滚动恢复及后台任务所有权。
对话框与任务: QDialog/QDialogButtonBox 只承载必须阻塞的决策;普通状态与可恢复错误使用页面内 Banner、状态区或非模态面板。长任务由 worker 或线程池执行,通过队列信号回传进度与结果,并支持取消、重复提交防护、关闭后忽略回调及成功/失败/重试状态。
多 Tab 数据工作台: 每个页面统一采用垂直布局:标题与说明、顶部 QToolBar/命令区、来源或校验状态区、可伸展的 QTableView 主区、底部选择摘要与完成操作区。将同一 QAction 复用于按钮、菜单和快捷键。普通成功不弹 QMessageBox;只有不可逆、高代价或缺少安全默认值的操作才在执行前确认。
3. 架构、对象生命周期与数据模型
对象与职责
- 让 View 负责渲染和输入转发,让领域/应用服务负责业务规则、I/O 和持久化。
- 使用
QObject的父子所有权管理 Qt 对象生命周期;不要同时建立不必要的 Python 强引用环。 - 对需要延迟销毁的
QObject使用deleteLater(),尤其是跨事件循环或线程退出时。 - 将共享用户操作建模为
QAction,避免在多个按钮和菜单处理器中复制逻辑。 - 在关闭窗口前处理未保存更改,但不要把业务保存逻辑塞入
closeEvent()。
信号与槽
- PySide 类中将作为连接目标的方法标记为
@Slot(...);官方建议这样做以减少运行时开销,并正确表达线程亲和性。 - 让信号载荷传递不可变数据、标识符或轻量结果,不要跨线程传递需要在工作线程访问的 GUI 对象。
- 明确连接的所有者和断开时机,避免重复连接导致一次操作执行多次。
- QML 可见属性必须提供通知信号,否则绑定不会可靠更新。
Model/View
- 动态、共享、可排序筛选或可能变大的数据使用
QListView、QTableView、QTreeView与QAbstractItemModel派生模型。 - 一维和二维数据分别优先从
QAbstractListModel、QAbstractTableModel开始,不要无必要直接实现完整QAbstractItemModel。 - 使用
QSortFilterProxyModel处理排序和筛选,不要为每次查询复制整个源数据集合。 - 插入、删除、移动和重置模型时使用正确的 begin/end 通知;不得只修改 Python 列表后强制刷新视图。
- 自定义绘制和编辑使用
QStyledItemDelegate;不要为每个单元格创建常驻 QWidget。 QListWidget、QTableWidget、QTreeWidget仅适合小型、简单、不会共享模型的数据。
4. 布局、窗口化与高 DPI
布局
- 使用
QLayout派生布局、QSizePolicy、stretch、sizeHint()和最小尺寸,不要用setGeometry()手工排列常规界面。 - 需要随宽度换行的自定义控件实现合理的
heightForWidth();内容变化时调用updateGeometry()。 - 为窗口定义可用的最小尺寸,而不是固定尺寸。长文本、翻译和字体放大后仍应可操作。
- 在少量语义断点下重排或显示/隐藏次要面板;不要在每次
resizeEvent()中重建整棵 Widget 树。 - 响应式切换时保留焦点、选择、滚动、编辑和展开状态。
主窗口与窗口状态
QMainWindow只把一个控件设置为centralWidget,再通过 Qt 提供的 API 管理菜单栏、工具栏、状态栏和QDockWidget。- 使用
saveGeometry()/restoreGeometry()和QMainWindow.saveState()/restoreState()持久化窗口与停靠布局。 - 恢复窗口时检查当前屏幕可用区域,防止窗口因显示器移除或 DPI 变化而出现在屏幕外。
- 为辅助窗口定义所有者、模态性、任务栏显示、激活、关闭和共享数据生命周期。
- 优先保留原生窗口框架。无边框窗口需要自行正确实现缩放命中测试、拖动、系统菜单、标题按钮、Snap Layouts、高 DPI 和无障碍,除非产品价值足以承担成本,否则不要使用。
高 DPI 与多显示器
- Qt 6 以设备无关像素描述大多数 Widget、Quick、事件和窗口几何;不要再次手工乘系统缩放比例。
- 自定义像素绘制或图像几何时读取
devicePixelRatio();不要把物理像素当作布局单位。 - 使用 SVG、
QIcon多尺寸资源或@2x高分辨率资源。验证 100%、125%、150%、200% 以及跨显示器移动。 - 屏幕几何使用
QScreen的设备无关坐标;不要假设多个屏幕组成无间隙的物理像素网格。
5. 主题、QSS、图标与 Windows 材质
主题优先级
- 优先让
QStyle和系统QPalette提供平台行为与控件状态。 - 用语义 Token 生成有限的自定义
QPalette或组件级 QSS。 - 只有品牌需要时才替换标准控件绘制或实现自定义
QStyle。
QPalette 为活动、非活动和禁用状态提供颜色组。覆盖颜色时同时定义这些状态,并独立验证浅色、深色和高对比度。
Qt 6.5 或更高版本可通过 QGuiApplication.styleHints().colorScheme() 读取系统配色方案,并监听 colorSchemeChanged。处理主题变化时也要响应 QEvent.PaletteChange 或 QEvent.ApplicationPaletteChange。使用前检查项目 Qt 版本;较旧版本不得生成不存在的 API。
Qt 6.10 或更高版本提供更多平台无障碍提示;低版本仍必须通过实际 Windows 高对比度测试验证,而不是自行猜测系统颜色。
QSS
- QSS 不是完整 CSS:不要期待 Web 布局、CSS 变量、继承和选择器行为完全一致。
- 避免
QWidget { ... }等大范围规则覆盖所有标准状态。 - 为 hover、pressed、checked、selected、disabled、focus、active/inactive 和高对比度定义可辨识行为。
- 使用动态属性选择器时,属性变化后按项目需要重新 polish;不要依赖偶然刷新。
- 样式表可能改变原生绘制和尺寸提示。修改后重新测试菜单、滚动条、输入法、焦点框、停靠窗口和高 DPI。
图标与 Windows 材质
- 使用一致的 SVG/
QIcon资源,为 normal、disabled、active、selected 等模式提供可读变体。 - 不要用 Emoji 作为结构图标,也不要依赖某台开发机才存在的字体图标。
- Qt Widgets/PySide 没有与 WinUI
MicaBackdrop完全等价的跨版本 API。需要 Mica/Acrylic 时,把 Windows 原生集成隔离为可选适配层,按 Windows/Qt 版本检查,并始终提供纯色QPalette回退。 - 不要为装饰效果移除原生标题栏、破坏高对比度或引入不可维护的全局事件过滤器。
6. 命令、键盘与无障碍
命令与键盘
- 用
QAction集中管理文本、图标、启用/选中状态和快捷键,并复用于菜单、工具栏、按钮或上下文菜单。 - 标准操作优先使用
QKeySequence.StandardKey,避免硬编码与平台约定冲突的组合键。 - 使用
QShortcut时设置合适的Qt.ShortcutContext;不要让页面快捷键意外覆盖整个应用。 - 使用标签中的
&和QLabel.setBuddy()提供访问键。处理中文界面时仍要检查访问键是否重复和是否可发现。 - 优先使用自然 Tab 顺序,仅在布局顺序无法表达正确流程时使用
QWidget.setTabOrder()。 - 所有悬停、右键和拖拽加速操作都必须有可见或键盘等价路径。
无障碍
- 标准 Widgets 与 Qt Quick Controls 已提供基础无障碍接口,优先复用它们。
- 仅在可见文本无法形成正确名称时设置
accessibleName;把补充说明放入accessibleDescription。 - 为表单建立标签 Buddy 和合理焦点顺序;不要只通过占位符标识输入字段。
- 自定义 QWidget 需要公开正确角色、名称、状态、值、操作和事件;复杂控件根据需要实现
QAccessibleInterface/QAccessibleWidget。 - QML 自定义项使用
Accessible附加属性公开角色、名称、描述和操作。 - 用 Narrator、Accessibility Insights/Inspect、键盘、高对比度和文本/显示缩放验证真实体验。
7. 线程、异步与响应速度
GUI 线程规则
QWidget及其子类不是可重入对象,只能在 GUI 主线程创建和访问。Qt Quick 场景对象也不得从工作线程直接修改。- 后台工作使用 worker
QObject+moveToThread(),或QThreadPool+QRunnable;根据项目复杂度选择一种模式并统一。 - 通过默认 AutoConnection 或显式 QueuedConnection 的信号把结果送回 GUI 线程。
- 对具有事件循环的接收者使用绑定对应的 Slot 装饰器,确保代码在接收对象所属线程执行。
- 在线程退出时使用
quit()、wait()和deleteLater()建立明确的清理链;关闭窗口后不得让回调访问已销毁 UI。 - 不要对同一线程使用 BlockingQueuedConnection,这会导致死锁。
任务设计
- 长任务提供进度、取消和最终状态;取消必须检查任务是否仍可安全提交结果。
- 搜索、预览和重新加载使用请求 ID 或取消令牌忽略过期结果。
- 网络请求优先使用事件驱动的
QNetworkAccessManager,或在工作线程使用不会触碰 GUI 的客户端。 - 不要在槽函数中执行阻塞 I/O,也不要用频繁
processEvents()掩盖冻结。 - 需要
asyncio时沿用项目已有事件循环集成。采用PySide6.QtAsyncio前核对精确版本和支持范围,不要同时运行两个互相竞争的主事件循环。
8. Designer、资源、设置与本地化
- Qt Widgets Designer 生成的
.ui文件应通过pyside6-uic/pyuic6生成代码,或由项目统一使用QUiLoader运行时加载。 - 不要手工编辑生成的
ui_*.py;在继承/组合的业务类中扩展界面,重新生成时避免丢失修改。 - PySide 官方建议优先使用与绑定版本匹配的
pyside6-uic,避免直接调用不匹配的独立uic。 - 使用
.qrc与pyside6-rcc/对应 PyQt 工具管理图标和资源,或采用项目已经验证的包资源方案。 - 使用
QSettings前设置组织名、域和应用名;为设置键、默认值、版本迁移和损坏恢复建立规范。 - 使用
tr()/翻译上下文和QTranslator进行本地化。不要拼接句子;测试复数、快捷键、长文本和从右到左布局。
9. 部署与测试
部署
- 固定 Python、绑定、Qt 和打包工具版本,并在干净虚拟环境中构建。
- PySide6 优先评估官方
pyside6-deploy;它使用pysidedeploy.spec保存配置,并支持--dry-run查看实际命令。 - PyQt 使用项目许可和工具链已验证的打包方案;不要把 PySide 部署命令用于 PyQt。
- 检查 Qt 平台插件、图像格式插件、QML imports、翻译、OpenSSL/多媒体依赖、MSVC Runtime、应用图标和资源是否完整。
- 在干净 Windows 虚拟机上测试首次启动、无开发环境运行、非 ASCII 路径、只读目录、多用户配置、升级与卸载。
- 发布前确认代码签名、安装器、文件关联、协议处理、自动更新和许可文件要求。
测试
- 使用 Qt Test/QTest 或项目现有测试栈覆盖关键 Widget、快捷键、模型通知和信号行为。
- 对 Model/View 测试插入、删除、移动、排序、筛选、编辑、空数据和超大数据。
- 对线程测试取消、窗口提前关闭、过期结果、异常和应用退出。
- 执行 Windows 测试矩阵:最低支持版本与当前 Windows 11;100%–200% 缩放;跨显示器;浅色、深色、高对比度;键盘、鼠标和声明支持的触控;Narrator。
- 测试打包产物,而不只测试源码环境。
10. 常见反模式
- 在同一项目中混用
PySide6与PyQt6导入。 - 从工作线程调用 Widget 方法或把 Widget 传给 worker。
- 用
time.sleep()、同步网络或大循环阻塞 GUI 线程。 - 为动态表格使用
QTableWidget并为每个单元格嵌入 Widget。 - 在每次窗口调整大小时销毁并重建全部页面。
- 用固定坐标、固定字体像素或手动 DPI 倍增代替 Qt 布局系统。
- 用全局 QSS 抹掉原生焦点、禁用、选择、高对比度和非活动窗口状态。
- 手工编辑
pyside6-uic/pyuic6生成文件。 - 无版本检查地复制 Qt C++、旧 PySide2、PyQt5 或另一个绑定的示例。
- 为获得 Mica/Acrylic 无条件启用无边框窗口或私有 Windows API。
11. 交付检查清单
- 已确认绑定、精确版本、Qt Widgets/QML 路线和许可证。
- 未混用 PySide/PyQt 或绑定特有 API。
- 标准控件、Model/View、
QAction和布局系统得到优先使用。 - GUI 对象只在主线程访问,后台结果通过队列信号返回。
- 主题跟随系统,QSS 未破坏高对比度、焦点或禁用状态。
- 窗口在调整大小、贴靠、跨显示器和 100%–200% 缩放时可用。
- 键盘、上下文菜单、拖拽替代方式和无障碍语义完整。
- 加载、空、错误、取消、超时、过期结果和撤销状态已处理。
- Designer 生成代码、资源和设置具有可重复的生成/迁移流程。
- 已在干净 Windows 环境测试最终打包产物。