Qt有了新的版本(第6版),PyQt和PySide也随之有了新的版本--现在分别称为PyQt6和PySide6。在准备Qt6版本的PyQt5和PySide2书籍时,我一直在研究这些库的最新版本,以确定它们之间的差异,并为编写可移植代码找到解决方案。
对PyQt6或PySide6的书感兴趣?PySide6版现在已经上市了!如果你购买了这个版本或者PyQt5版本,你也会在PyQt6版本上市时得到它
在这个简短的指南中,我将介绍为什么会有两个库,你是否需要关心(剧透:你不需要),有什么区别以及如何解决它们。到最后,你应该能够自如地重复使用PyQt6和PySide6教程中的代码示例来构建你的应用程序,无论你自己使用的是哪一个软件包。
背景介绍
为什么有两个库?
PyQt是由Riverbank Computing Ltd.的Phil Thompson开发的,并且已经存在了很长时间--支持Qt的版本可以追溯到2.x。2009年,当时拥有Qt工具包的Nokia想以更宽松的LGPL许可来提供Qt的Python绑定。
它被称为PySide,因为 "side "是芬兰语的 "binder"。
这两个接口基本上是等价的,但随着时间的推移,PySide的发展落后于PyQt。这一点在Qt 5发布后尤为明显--PyQt的Qt5版本(PyQt5)从2016年中开始提供,而PySide的第一个稳定版本是在两年后。 然而,Qt项目最近将PySide作为Python的官方Qt版本,这应该确保其未来的生存能力。当Qt6发布时,这两个Python绑定都在不久之后可用。
| PyQt6 | PySide6 | |
|---|---|---|
| 第一个稳定版本 | 2021年1月 | 2020年12月 |
| 开发单位 | Riverbank Computing Ltd. | Qt |
| 许可证 | GPL或商业 | LGPL |
| 平台 | Python 3 | Python 3 |
您应该使用哪一个?嗯,说实话,这并不重要。
这两个包都包裹着同一个库--Qt6--因此有99.9%的相同的API(见下面的少数区别)。你在一个库中学到的任何东西都可以很容易地应用到使用另一个库的项目中。此外,无论你选择使用哪一个,都值得熟悉另一个,这样你就可以最好地利用所有可用的在线资源--例如,使用PyQt6教程来构建你的PySide6应用程序,反之亦然。
在这个简短的概述中,我将介绍这两个包之间的一些明显的区别,并解释如何编写与这两个包无缝工作的代码。读完后,你应该能够在网上找到任何PyQt6的例子,并将其转换为PySide6的工作。
许可证
这两个版本之间的主要明显区别是许可--PyQt6在GPL或商业许可下可用,而PySide6在LGPL许可下可用。
如果你打算在GPL下发布你的软件,或者你正在开发的软件不会被分发,PyQt6的GPL要求不太可能成为一个问题。然而,如果你想发布你的软件但不分享你的源代码,你将需要从Riverbank购买PyQt6的商业许可或使用PySide6。
Qt本身在Qt商业许可、GPL 2.0、GPL 3.0和LGPL 3.0许可下可用。
更多信息请参见我的FAQ,关于PyQt/PySide应用中GPL与LGPL许可的影响。
命名空间和枚举
PyQt6引入的主要变化之一是需要为枚举和标志使用完全合格的名称。以前,在PyQt5和PySide2中,你可以使用快捷键--例如Qt.DecorationRole,Qt.AlignLeft 。在PyQt6中,这些名字现在分别是Qt.ItemDataRole.DisplayRole 和Qt.Alignment.AlignLeft 。这一变化影响了Qt中所有的枚举和标志组。在PySide6中,长名和短名都被支持。
由于PyQt6和PySide6对标志使用了微妙的不同的命名规则,情况变得有些复杂。在PySide6(和v5)中,标志被分组在带有 "Flag "后缀的标志对象下,例如Qt.AlignmentFlag --向左对齐的标志是Qt.AlignmentFlag.AlignLeft 。在PyQt6中,同样的标志组只被命名为 "Qt.Alignment"。这意味着你不能简单地选择长或短的形式而保留PyQt6 & PySide6之间的兼容性。
UI文件
这两个库的另一个主要区别在于它们对加载从Qt Creator/Designer导出的.ui 文件的处理。PyQt6提供了uic 子模块,可以用来直接加载UI文件,以产生一个对象。这感觉很像Pythonic(如果你忽略了camelCase)。
蟒蛇
import sys
from PyQt6 import QtWidgets, uic
app = QtWidgets.QApplication(sys.argv)
window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()
与PySide6相当的是多了一行,因为你需要先创建一个QUILoader 对象。不幸的是这两个接口的API也是不同的(.load vs.loadUI )。
python
import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader()
app = QtWidgets.QApplication(sys.argv)
window = loader.load("mainwindow.ui", None)
window.show()
app.exec_()
要在PyQt6中加载一个UI到一个现有的对象上,例如在你的QMainWindow.__init__ ,你可以调用uic.loadUI ,将self (现有的widget)作为第二个参数。
import sys
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6 import uic
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
uic.loadUi("mainwindow.ui", self)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
PySide6加载器不支持这个--.load 的第二个参数是你正在创建的widget的父widget。这阻止了你向widget的__init__ 块添加自定义代码,但你可以通过一个单独的函数来解决这个问题。
import sys
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader()
def mainwindow_setup(w):
w.setWindowTitle("MainWindow Title")
app = QtWidgets.QApplication(sys.argv)
window = loader.load("mainwindow.ui", None)
mainwindow_setup(window)
window.show()
app.exec()
将UI文件转换为Python
两个库都提供了相同的脚本来从Qt Designer.ui 文件中生成可导入的Python模块。对于PyQt6,该脚本被命名为pyuic5 --
bash
pyuic6 mainwindow.ui -o MainWindow.py
然后你可以导入UI_MainWindow 对象,从你正在使用的基类中使用多重继承的方式进行子类化(例如:QMainWIndow ),然后调用self.setupUI(self) 来设置用户界面。
import sys
from PyQt6 import QtWidgets
from MainWindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setupUi(self)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
对于PySide6,它被命名为pyside6-uic 。
bash
pyside6-uic mainwindow.ui -o MainWindow.py
随后的设置是相同的。
import sys
from PySide6 import QtWidgets
from MainWindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setupUi(self)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
exec() 或 exec_()
.exec() 方法在Qt中被用来启动你的QApplication 或对话框的事件循环。在Python 2.7中,exec 是一个关键字,这意味着它不能用于变量、函数或方法名。在PyQt4和PySide中使用的解决方案是将.exec 的使用重命名为.exec_() ,以避免这种冲突。
Python 3 删除了exec 关键字,释放了这个名字的使用。因此,从PyQt6开始,.exec() 的调用就像在Qt中一样被命名。然而,PySide6仍然使用.exec_() 。
插槽和信号
定义自定义槽和信号在这两个库中使用的语法略有不同。PySide6以Signal 和Slot 的名义提供这个接口,而PyQt6则分别以pyqtSignal 和pyqtSlot 的名义提供。在定义和槽和信号方面,它们的行为是相同的。
下面的PyQt6和PySide6的例子是相同的---
my_custom_signal = pyqtSignal() # PyQt6
my_custom_signal = Signal() # PySide6
my_other_signal = pyqtSignal(int) # PyQt6
my_other_signal = Signal(int) # PySide6
或者对于一个槽--
@pyqtslot
def my_custom_slot():
pass
@Slot
def my_custom_slot():
pass
如果你想确保PyQt6和PySide6之间的一致性,你可以对PyQt6使用下面的导入模式,在那里也使用Signal 和@Slot 样式。
from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
当然,你也可以做相反的事情from PySide6.QtCore import Signal as pyqtSignal, Slot as pyqtSlot ,尽管这有点混乱。
QMouseEvent
在PyQt6中,QMouseEvent 对象不再有.pos(),.x() 或.y() 的速记属性方法来访问事件的位置。你必须使用.position() 属性来获得一个QPoint 对象并访问其上的.x() 或.y() 方法。.position() 方法在PySide6中也是可用的。
PySide6中的功能,但在PyQt6中没有。
从Qt 6开始,PySide支持两个Python__feature__ 标志,以帮助使代码更具有Pythonic的 snake_case 变量名,并能够直接分配和访问属性,而不是使用getter/setter函数。下面的例子显示了这些变化对代码的影响 --
table = QTableWidget()
table.setColumnCount(2)
button = QPushButton("Add")
button.setEnabled(False)
layout = QVBoxLayout()
layout.addWidget(table)
layout.addWidget(button)
同样的代码,但启用了snake_case 和true_property 。
from __feature__ import snake_case, true_property
table = QTableWidget()
table.column_count = 2
button = QPushButton("Add")
button.enabled = False
layout = QVBoxLayout()
layout.add_widget(table)
layout.add_widget(button)
这些特征标志对于代码的可读性是一个很好的改进,然而由于PyQt6中不支持它们,这使得编写可移植的代码更加困难。
在库中支持两者
如果你在写一个独立的应用程序,你不需要担心这个问题,只要使用你喜欢的API即可。
如果你正在编写一个库、widget或其他工具,你想同时兼容PyQt6和PySide6,你可以通过添加两套导入来轻松做到。
import sys
if 'PyQt6' in sys.modules:
# PyQt6
from PyQt6 import QtGui, QtWidgets, QtCore
from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
else:
# PySide6
from PySide6 import QtGui, QtWidgets, QtCore
from PySide6.QtCore import Signal, Slot
这就是我们的自定义widgets库中使用的方法,我们通过一个库的导入支持PyQt6和PySide6。唯一需要注意的是,在导入这个库的时候,你必须确保PyQt6是先导入的(如在上面那一行或更早),以确保它在sys.modules 。
为了说明PyQt6中缺少速记枚举和标志,你可以自己生成这些。例如,下面的代码将把每个枚举对象元素的引用复制到它们的父对象中,使它们可以像PyQt5、PySide2和PySide6那样被访问。这段代码只需要在PyQt6下运行。
enums = [
(QtCore.Qt, 'Alignment'),
(QtCore.Qt, 'ApplicationAttribute'),
(QtCore.Qt, 'CheckState'),
(QtCore.Qt, 'CursorShape'),
(QtWidgets.QSizePolicy, 'Policy'),
]
# Look up using the long name (e.g. QtCore.Qt.CheckState.Checked, used
# in PyQt6) and store under the short name (e.g. QtCore.Checked, used
# in PyQt5, PySide2 & accepted by PySide6).
for module, enum_name in enums:
for entry in getattr(module, enum_name):
setattr(module, entry.name, entry)
另外,你也可以定义一个自定义函数来处理命名空间的查找。
def _enum(obj, name):
parent, child = name.split('.')
result = getattr(obj, child, False)
if result: # Found using short name only.
return result
obj = getattr(obj, parent) # Get parent, then child.
return getattr(obj, child)
当传递一个对象和一个与PyQt6兼容的长格式名称时,这个函数将在PyQt6和PySide6上返回正确的枚举或标志。
>>> _enum(PySide6.QtCore.Qt, 'Alignment.AlignLeft')
PySide6.QtCore.Qt.AlignmentFlag.AlignLeft
>>> _enum(PyQt6.QtCore.Qt, 'Alignment.AlignLeft')
<Alignment.AlignLeft: 1>
最后一个复杂的问题是exec_() 和exec() 方法调用中的不匹配。你可以通过实现一个函数来检查每个方法的存在并调用存在的方法来解决这个问题。
def _exec(obj):
if hasattr(obj, 'exec'):
return obj.exec()
else:
return obj.exec_()
然后你可以用这个函数在PyQt6和PySide6上以可移植的方式执行 QApplication 和QDialog 这样的对象。
app = QApplication(sys.argv)
_exec(app)
如果你在多个文件中这样做,会变得有点麻烦。一个很好的解决方案是将导入逻辑和自定义shim方法移到他们自己的文件中,例如在你的项目根中命名为qt.py 。这个模块从两个库中的一个导入Qt模块(QtCore,QtGui,QtWidgets, 等等),然后你再从那里导入到你的应用程序中。
qt.py 的内容与我们之前使用的相同 -
import sys
if 'PyQt6' in sys.modules:
# PyQt6
from PyQt6 import QtGui, QtWidgets, QtCore
from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
else:
# PySide6
from PySide6 import QtGui, QtWidgets, QtCore
from PySide6.QtCore import Signal, Slot
def _enum(obj, name):
parent, child = name.split('.')
result = getattr(obj, child, False)
if result: # Found using short name only.
return result
obj = getattr(obj, parent) # Get parent, then child.
return getattr(obj, child)
def _exec(obj):
if hasattr(obj, 'exec'):
return obj.exec()
else:
return obj.exec_()
你必须记得在if块的两个分支中加入你使用的任何其他PyQt6模块(浏览器、多媒体等)。
from .qt import QtGui, QtWidgets, QtCore, _enum, _exec
......并且它将在两个库中无缝工作。
就这样了
没有什么可说的了--这两个库真的很相似。然而,如果你遇到任何其他PyQt6/PySide6的例子或功能不能轻易转换,请给我留言。