Qt for Python 6.12 随 Qt 6.12 LTS 一同发布,是近年来规模最大的一次版本更新。如果尚未了解 Qt 新版本的变化,请查阅整体发布公告。
以下几项尤为突出:
- 面向 QML 的响应式属性:全新的 QtQmlFeatures 模块可省去 Python 类中大部分信号与属性的模板代码。
- PySide6 支持 iOS:期待已久的 iOS 支持现已推出。
- 由《欧盟网络弹性法案》(CRA)推动的工具链安全审查:我们对 Qt for Python 进行了威胁建模,修复了发现的问题,并在源文件中添加了安全标记。
- Python 3.15:基于此前的某个候选发布版(RC)提供初步支持;自由线程(free-threaded)版 Python 的支持仍在研究中。
此外,本次发布还包含 Qt for Python 各部分的大量 bug 修复。
QtQmlFeatures:面向 Python 的响应式属性(技术预览版)
将一个 Python 值暴露给 QML 所需的代码比实际应有的要多。仅一个 price 属性,就需要一个 Signal、一个后端字段、一个 getter,以及一个检查值的变更并发出信号的 setter。对于派生值还需要更多额外的关联代码。
全新的 QtQmlFeatures 模块可省去其中大部分代码。下面是包含 price、quantity 以及派生值 total 的购物车示例,先看传统写法:
class Cart(QObject): priceChanged = Signal() quantityChanged = Signal() totalChanged = Signal() def __init__(self, parent=None): super().__init__(parent) self._price = 10 self._quantity = 2 self._total = self._price * self._quantity self.priceChanged.connect(self._recompute_total) self.quantityChanged.connect(self._recompute_total) @Property(int, notify=priceChanged) def price(self): return self._price @price.setter def price(self, value): if self._price == value: return self._price = value self.priceChanged.emit() # ...the same again for quantity and total |
下面是使用新装饰器实现的同一个购物车代码,同时它还会在价格大幅上涨时发出警告,并记录每一次变更。传统写法则需要在每个 setter 中分别实现这些逻辑。
@auto_properties class Cart(QObject): def __init__(self, parent=None): super().__init__(parent) self.price = 10 self.quantity = 2 @computed("price", "quantity") def total(self) -> int: return self.price * self.quantity @watch("price") def on_price_changed(self, change: Change): if change.new > change.old * 1.5: print(f"warning: price jumped {change.old} -> {change.new}") @effect("price", "quantity") def log_state(self): print(f"cart: {self.price} x {self.quantity}") |
这些属性仍是真正的 Q_PROPERTY,QML 可对其进行读写。当 QML 修改 price 时,监听器随即触发,total 随之更新,绑定也会刷新。整个过程无需编写任何信号连接的代码。
以下四个装饰器完成了这些工作:
@auto_properties 将普通的 self.x = ... 赋值转换为带通知信号的属性。
@computed 声明一个带缓存的只读值,依赖项发生变化时自动刷新。
@watch 在属性变化后运行,并提供变化前后的值。
@effect 在所列属性中任意一个发生变化时运行。
响应式属性教程会逐步构建这个购物车示例:从传统写法出发,每一步去掉一部分模板代码。此外,以下示例已更新为使用 QtQmlFeatures:
此外还新增了两个示例:
- QML 组件加载示例:演示如何通过
load_qml_component 从 Python 加载 QML 定义的类型
- 在 Python 中使用 Quick Controls:演示如何通过
load_qml_component 从 Python 加载 QtQuick Controls 类型,并在 Python 中驱动其功能
iOS 支持:进一步增强移动端兼容性
我们此前提到 iOS 支持正在推进,现在很高兴地宣布:iOS 支持已经加入(PYSIDE-2352)。感谢一路走来为达成这一里程碑做出贡献的所有人。
入门只需三步:
- 获取 iOS wheel 包。
iOS 需要专用的 PySide6 和 shiboken6 wheel 包,可从 Qt for Python 下载页面下载,也可通过 qtpip 获取:
|
$ pip install qtpip
$ qtpip download PySide6 --ios --arch arm64 --platform device
|
使用 --platform simulator 可改为获取适用于 iOS 模拟器的 wheel 包。每个 wheel 包仅针对一种架构和一个平台。
- 生成 Xcode 工程。
全新的 pyside6-ios-deploy 工具会根据项目和这两个 wheel 包生成 Xcode 工程。它与 pyside6-deploy 和 pyside6-android-deploy 使用同一个 pysidedeploy.spec 文件。
|
$ pyside6-ios-deploy /path/to/main.py --name "<AppName>" \
--wheel-pyside=<PySide6 wheel> \
--wheel-shiboken=<shiboken wheel>
|
该工具需要在 macOS 主机上运行,首次使用时会自动下载 Python.xcframework。
- 在 Xcode 中构建并运行。
打开生成的 .xcodeproj,即可在模拟器或已连接的设备上运行。签名与配置描述文件仍在 Xcode 中处理。构建真机版本时,请通过 --team-id 传入 Apple Developer Team ID。
完整说明请参阅 pyside6-ios-deploy 文档,也可阅读专题文章《Python 移动应用开发:将 PySide6 带到 iOS》,该文已更新至最新功能和选项。
部署:更顺畅,覆盖更多平台
本次发布对部署环节做了诸多改进。
pyside6-deploy
- 现可通过
pyproject.toml 中的 [tool.pyside6.deploy] 部分进行配置(PYSIDE-1612)。
pyside6-deploy、pyside6-android-deploy 以及 pyside6-project deploy 支持 --no-install 参数,可跳过构建依赖项的自动安装。
- ⚠️ 行为变更:
pyside6-deploy 现在默认生成独立(standalone)包(PYSIDE-3459)。如需沿用之前的默认行为,请使用 --onefile。
Android
- NDK 已更新至 r28c,limited API wheel 标签提升至 cp310。
- 支持 3.10 至 3.14 的主机 Python 版本,生成的二进制文件兼容使用 16 KB 内存页的设备(PYSIDE-3425)。
- 工具现在会在未满足前置条件时给出提示,而不再卡住,并能稳定部署使用 QtQmlFeatures 的应用。
- 现改用预编译的 CPython,取代手动交叉编译(PYSIDE-3426)。
- 修复了 JDK 查找问题,以及 macOS 上 NDK 被隔离(quarantine)的问题。
安全加固
随着《欧盟网络弹性法案》(CRA)的报告义务开始适用,安全问题受到了更多关注。安全一直是 Qt for Python 的重点。在 6.12 中,我们以攻击者的视角审视自身工具,并对 Qt for Python 完成了全面的威胁分析。
其中对开发者体验有一定影响的主要变化包括:
- Android 部署:修复了 wheel 包解压过程中潜在的 zip slip 问题;下载的 NDK 和命令行工具现在会先经过校验再解压。
- pyside6-qml:导入与 QML 源文件位于同一目录的 Python 文件前,现在会发出警告。
- pyside6-deploy:修复了 Windows 上潜在的 shell 注入问题。
- 序列化:现在会阻止从 QDataStream 和 QSettings 反序列化非简单类。
- Qt Widgets Designer:加载 Python 插件前,现在会要求一次性确认。
- QUiLoader:
loadUiType() 不再在系统路径中搜索 pyside6-uic 二进制文件。现在需通过关键字参数显式启用,并会触发警告。我们建议尽量不再使用该函数,改为在构建时运行 pyside6-uic。
- Shiboken6:生成 doxygen 文档时,对 libxml ≥ 2.13 使用 XML_PARSE_NO_XXE 解析 XML,对更早版本则禁用实体扩展。
⚠️ 软件物料清单(SBOM)文档尚未就绪,我们将在后续版本中添加。
Qt WebEngine 与 Qt PDF 的全新分发方式
如果使用 Qt WebEngine,可能已经读过宣布发布节奏拆分的博客文章。为此,我们采用相同的配置,确保开发者能随 PySide 版本同步获得最新版本。
由于 wheel 包包含各模块的二进制文件,我们将采用的方案是:wheel 包的版本号由 Qt 完整版本号加 Chromium 版本号组合而成。这意味着,对于 Qt WebEngine 6.140,我们会在 6.12 中发布版本号为 6.12.0.140 的包,从而既能随 Qt 的缺陷修复版本发布新的 wheel 包,也能随 WebEngine 的 Chromium 新版本发布新的 wheel 包。
Qt PDF 模块的自然依赖项同样受这一新结构影响,并将沿用相同的配置。
Python:新版本与改进
- Python 3.15:已加入初步支持(PYSIDE-3221)。Shiboken 会在针对 3.15 的条件分支中生成新的模块初始化代码。
- 类型提示:QDate 和 QDateTime 现在接受 Python 的 datetime 类型(PYSIDE-3038),并修复了 __copy__ 的返回值注解(PYSIDE-3338)。
- 新增 API:新增 QBitArray.fromBits(PyBuffer) 和 QBitArray.bits()(PYSIDE-3335)。QMetaObject::className() 现在会为嵌套类返回限定名称,例如 Outer.Inner(PYSIDE-3294)。
- 诊断:遇到未知的信号参数时,现在会显示警告(PYSIDE-3364)。
- 自由线程 Python:Quansight 正在持续推进让 PySide 支持自由线程,希望很快能有新进展。
稳定性修复
- 修复通过
invisibleRootItem() 填充数据后调用 clear() 时,QStandardItemModel 出现的内存泄漏(PYSIDE-3365)
- 修复从
QAbstractItemModel.data() 返回实现序列协议的类时出现的崩溃(PYSIDE-3423)
- 修复获取
QtWebEngineCore.ResourceRequestBody 的包装对象时出现的崩溃(PYSIDE-3409)
- 修复因父子关系设置不当导致
QLayoutItem 指针失效而引发的崩溃(PYSIDE-3469)
- 修复由派生类调用的私有方法(以
__ 为前缀)建立连接的问题(PYSIDE-3376)
- 修复影响头文件打包和 include 路径的 CMake 构建回归问题(PYSIDE-3306)
⚠️ 已移除 QTreeWidgetItem、QTableWidgetItem 和 QListWidgetItem 的拷贝构造函数(PYSIDE-3367)。
Shiboken6 更新
- 新增类型系统属性,用于在自由线程构建中控制 GIL 的使用(PYSIDE-2221)
- 现在可以通过函数修改(function modification)标记
QEventLoop.exec() 等阻塞函数(PYSIDE-3356)
- 相关的 Nuitka 缺陷修复后,针对已部署场景也会生成多阶段模块初始化代码(PYSIDE-3155)
- 修复了 shiboken6 的
ObjectType/Object 类型的 __module__ 属性(PYSIDE-3336)
- shibokenmodule:新增 dump() 的递归版本,用于检查父子关系(PYSIDE-3469)
获取 Qt for Python
与往常一样,运行以下命令即可获取最新版本:
pip install pyside6
如果使用 uv 命令,请将其添加到项目中并同步环境:
uv add pyside6
uv sync
如果您是商业版用户,则运行:
qtpip install pyside6
保持联系!
我们希望持续尝试新的实验性支持、功能以及 Python 模块集成。接下来应该做什么?欢迎给我们留言,或在 Qt Bug Tracker 上提交建议 👍。
希望大家喜欢这次发布。与往常一样,欢迎访问我们的社区平台;如遇问题,请提交缺陷报告告知我们。