PyQt6 против PySide6
Есть новая версия Qt (версия 6), а вместе с ней и новые версии PyQt и PySide, которые теперь называются PyQt6 и PySide6 соответственно. При подготовке к выпуску Qt6 моих книг PyQt5 и PySide2 я просматривал последние версии библиотек, чтобы определить различия между ними и найти решения для написания переносимого кода.
В этом кратком руководстве я расскажу, почему существуют две библиотеки, нужно ли вам заботиться (спойлер: нет), каковы различия и как их обойти. К концу вам должно быть удобно повторно использовать примеры кода из руководств PyQt6 и PySide6 для создания своих приложений, независимо от того, какой пакет вы используете.
Истоки
Почему две библиотеки?
PyQt разработан Филом Томпсоном из Riverbank Computing Ltd. и существует очень долгое время - поддерживая версии Qt, начиная с 2.x. В 2009 году Nokia, которой в то время принадлежал инструментарий Qt, захотела сделать привязки Python для Qt доступными в рамках более разрешительной лицензии LGPL.
Он называется PySide, потому что «side» по-фински означает «связующее».
Эти два интерфейса были в основном эквивалентными, но со временем разработка PySide отставала от PyQt. Это было особенно заметно после выпуска Qt 5 - версия PyQt (PyQt5) для Qt5 была доступна с середины 2016 года, а первая стабильная версия PySide была выпущена двумя годами позже. Однако проект Qt недавно принял PySide в качестве официального выпуска Qt для Python, что должно гарантировать его жизнеспособность в будущем. Когда был выпущен Qt6, вскоре после этого были доступны обе привязки Python.
PyQt6 | PySide6 | |
---|---|---|
First stable release | Jan 2021 | Dec 2020 |
Developed by | Riverbank Computing Ltd. | Qt |
License | GPL or commercial | LGPL |
Platforms | Python 3 | Python 3 |
Что вам следует использовать? Ну, честно говоря, это не имеет значения.
Оба пакета содержат одну и ту же библиотеку - Qt6 - и поэтому имеют 99,9% идентичных API (некоторые различия см. Ниже). Все, что вы узнаете с помощью одной библиотеки, будет легко применить к проекту с использованием другой. Кроме того, независимо от того, какой из них вы выберете для использования, стоит ознакомиться с другим, чтобы вы могли наилучшим образом использовать все доступные онлайн-ресурсы - например, используя учебные пособия PyQt6 для создания приложений PySide6, и наоборот.
В этом кратком обзоре я рассмотрю несколько заметных различий между двумя пакетами и объясню, как написать код, который без проблем работает с обоими. Прочитав это, вы сможете взять любой пример PyQt6 в Интернете и преобразовать его для работы с PySide6.
Лицензирование
Основное заметное различие между двумя версиями - это лицензирование: PyQt6 доступен по лицензии GPL или коммерческой лицензии, а PySide6 - по лицензии LGPL.
Если вы планируете выпустить свое программное обеспечение под лицензией GPL или разрабатываете программное обеспечение, которое не будет распространяться, требование GPL для PyQt6 вряд ли станет проблемой. Однако, если вы хотите распространять свое программное обеспечение, но не делитесь своим исходным кодом, вам необходимо приобрести коммерческую лицензию на PyQt6 у Riverbank или использовать PySide6.
Сам Qt доступен по коммерческой лицензии Qt, лицензиям GPL 2.0, GPL 3.0 и LGPL 3.0.
Пространства имен и перечисления
Одним из основных изменений, внесенных в 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 файлы
Еще одно важное различие между двумя библиотеками заключается в их обработке загрузки файлов .ui, экспортированных из Qt Creator / Designer. PyQt6 предоставляет подмодуль uic, который можно использовать для непосредственной загрузки файлов пользовательского интерфейса для создания объекта. Это выглядит довольно 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 и .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, например, в вашем QMainWindow .__ init__, вы можете вызвать uic.loadUI, передав self (существующий виджет) в качестве второго параметра.
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 - это родительский виджет того виджета, который вы создаете. Это предотвращает добавление пользовательского кода в блок __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
Обе библиотеки предоставляют идентичные сценарии для создания импортируемых модулей Python из файлов .ui Qt Designer. Для PyQt6 скрипт называется pyuic6 -
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 -
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_()
Компилятор пользовательского интерфейса PyQt имеет флаг -x, который можно использовать для создания исполняемой демонстрации из файла пользовательского интерфейса. Это недоступно в версии PySide.
exec() или exec_()
Метод .exec () используется в Qt для запуска цикла обработки событий вашего QApplication или диалоговых окон. В Python 2.7 exec было ключевым словом, что означало, что его нельзя было использовать для имен переменных, функций или методов. Решение, используемое как в PyQt4, так и в PySide, заключалось в переименовании использования .exec в .exec_ (), чтобы избежать этого конфликта.
Python 3 удалил ключевое слово exec, освободив имя для использования. В результате из PyQt6 вызовы .exec () именуются так же, как в Qt. Однако PySide6 по-прежнему использует .exec_ ().
Slots и Signals
Для определения пользовательских слотов и сигналов используется немного другой синтаксис между двумя библиотеками. 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
Конечно, вы могли бы сделать обратное из PySide6.QtCore import Signal как pyqtSignal, Slot как pyqtSlot, хотя это немного сбивает с толку.
QMouseEvent
В PyQt6 объекты QMouseEvent больше не имеют сокращенных методов свойств .pos (), .x () или .y () для доступа к позиции события. Вы должны использовать свойство .position (), чтобы получить объект QPoint и получить доступ к его методам .x () или .y (). Метод .position () также доступен в PySide6.
Свойства которые есть в PySide6 но нет в PyQt6
Начиная с Qt6 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.
PYTHON
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, который вам больше нравится.
Если вы пишете библиотеку, виджет или другой инструмент, и хотите что бы они были совместимыми как с 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
Чтобы учесть отсутствие сокращенного перечисления и флагов в 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_()
Затем вы можете использовать эту функцию для выполнения таких объектов, как QApplication и QDialog, переносимым способом как на PyQt6, так и на PySide6.
app = QApplication(sys.argv)
_exec(app)
Содержимое 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_()
Вы должны не забыть добавить любые другие используемые вами модули PyQt6 (браузер, мультимедиа и т. Д.) В обе ветви блока if. Затем вы можете импортировать Qt6 в свое собственное приложение следующим образом:
from .qt import QtGui, QtWidgets, QtCore, _enum, _exec
... и он будет без проблем работать с любой библиотекой.
Это действительно то что нужно
Больше нечего сказать - две библиотеки действительно настолько похожи. Однако, если вы наткнетесь на какие-либо другие примеры или функции PyQt6 / PySide6, которые вы не можете легко преобразовать, напишите мне.
Спасибо за статью.
ОтветитьУдалить