add PyQt6 support to Qt.py and functions.py

This commit is contained in:
KIU Shueng Chuan 2021-01-14 19:03:52 +08:00
parent ab41c03358
commit dcbddb0abf
2 changed files with 115 additions and 29 deletions

View File

@ -11,6 +11,8 @@ This module exists to smooth out some of the differences between PySide and PyQt
"""
import os, sys, re, time, subprocess, warnings
import importlib
import enum
from .python2_3 import asUnicode
@ -28,7 +30,7 @@ QT_LIB = os.getenv('PYQTGRAPH_QT_LIB')
## This is done by first checking to see whether one of the libraries
## is already imported. If not, then attempt to import PyQt4, then PySide.
if QT_LIB is None:
libOrder = [PYQT4, PYSIDE, PYQT5, PYSIDE2, PYSIDE6]
libOrder = [PYQT4, PYSIDE, PYQT5, PYSIDE2, PYSIDE6, PYQT6]
for lib in libOrder:
if lib in sys.modules:
@ -45,7 +47,7 @@ if QT_LIB is None:
pass
if QT_LIB is None:
raise Exception("PyQtGraph requires one of PyQt4, PyQt5, PySide, PySide2 or PySide6; none of these packages could be imported.")
raise Exception("PyQtGraph requires one of PyQt4, PyQt5, PyQt6, PySide, PySide2 or PySide6; none of these packages could be imported.")
class FailedImport(object):
@ -221,6 +223,25 @@ elif QT_LIB == PYQT5:
VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
elif QT_LIB == PYQT6:
from PyQt6 import QtGui, QtCore, QtWidgets, uic
try:
from PyQt6 import QtSvg
except ImportError as err:
QtSvg = FailedImport(err)
try:
from PyQt6 import QtOpenGLWidgets
except ImportError as err:
QtOpenGLWidgets = FailedImport(err)
try:
from PyQt6 import QtTest
QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed
except ImportError as err:
QtTest = FailedImport(err)
VERSION_INFO = 'PyQt6 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
elif QT_LIB == PYSIDE2:
from PySide2 import QtGui, QtCore, QtWidgets
@ -277,8 +298,8 @@ else:
raise ValueError("Invalid Qt lib '%s'" % QT_LIB)
# common to PyQt5, PySide2 and PySide6
if QT_LIB in [PYQT5, PYSIDE2, PYSIDE6]:
# common to PyQt5, PyQt6, PySide2 and PySide6
if QT_LIB in [PYQT5, PYQT6, PYSIDE2, PYSIDE6]:
# We're using Qt5 which has a different structure so we're going to use a shim to
# recreate the Qt4 structure
@ -353,13 +374,13 @@ if QT_LIB in [PYSIDE, PYSIDE2, PYSIDE6]:
QtTest.QTest.qWait = qWait
# Common to PyQt4 and 5
if QT_LIB in [PYQT4, PYQT5]:
# Common to PyQt4, PyQt5 and PyQt6
if QT_LIB in [PYQT4, PYQT5, PYQT6]:
QtVersion = QtCore.QT_VERSION_STR
try:
from PyQt5 import sip
except ImportError:
sip = importlib.import_module(QT_LIB + '.sip')
except ModuleNotFoundError:
import sip
def isQObjectAlive(obj):
return not sip.isdeleted(obj)
@ -369,6 +390,58 @@ if QT_LIB in [PYQT4, PYQT5]:
QtCore.Signal = QtCore.pyqtSignal
if QT_LIB == PYQT6:
# module.Class.EnumClass.Enum -> module.Class.Enum
def promote_enums(module):
class_names = [x for x in dir(module) if x[0] == 'Q']
for class_name in class_names:
klass = getattr(module, class_name)
if not isinstance(klass, sip.wrappertype):
continue
attrib_names = [x for x in dir(klass) if x[0].isupper()]
for attrib_name in attrib_names:
attrib = getattr(klass, attrib_name)
if not isinstance(attrib, enum.EnumMeta):
continue
for e in attrib:
setattr(klass, e.name, e)
promote_enums(QtCore)
promote_enums(QtGui)
promote_enums(QtWidgets)
# QKeyEvent::key() returns an int
# so comparison with a Key_* enum will always be False
# here we convert the enum to its int value
for e in QtCore.Qt.Key:
setattr(QtCore.Qt, e.name, e.value)
# shim the old names for QPointF mouse coords
QtGui.QSinglePointEvent.localPos = lambda o : o.position()
QtGui.QSinglePointEvent.windowPos = lambda o : o.scenePosition()
QtGui.QSinglePointEvent.screenPos = lambda o : o.globalPosition()
QtGui.QDropEvent.posF = lambda o : o.position()
QtWidgets.QApplication.exec_ = QtWidgets.QApplication.exec
QtWidgets.QDialog.exec_ = lambda o : o.exec()
QtGui.QDrag.exec_ = lambda o : o.exec()
# PyQt6 6.0.0 has a bug where it can't handle certain Type values returned
# by the Qt library.
try:
# 213 is a known failing value
QtCore.QEvent.Type(213)
except ValueError:
def new_method(self, old_method=QtCore.QEvent.type):
try:
typ = old_method(self)
except ValueError:
typ = QtCore.QEvent.Type.None_
return typ
QtCore.QEvent.type = new_method
del new_method
# USE_XXX variables are deprecated
USE_PYSIDE = QT_LIB == PYSIDE
USE_PYQT4 = QT_LIB == PYQT4

View File

@ -19,6 +19,7 @@ from pyqtgraph.util.cupy_helper import getCupy
from . import debug, reload
from .Qt import QtGui, QtCore, QT_LIB, QtVersion
from . import Qt
from .metaarray import MetaArray
from .pgcollections import OrderedDict
from .python2_3 import asUnicode, basestring
@ -1254,23 +1255,28 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
imgData = imgData.copy()
profile("copy")
if QT_LIB == 'PySide':
ch = ctypes.c_char.from_buffer(imgData, 0)
img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat)
elif QT_LIB in ['PySide2', 'PySide6']:
img = QtGui.QImage(imgData, imgData.shape[1], imgData.shape[0], imgFormat)
# C++ QImage has two kind of constructors
# - QImage(const uchar*, ...)
# - QImage(uchar*, ...)
# If the const constructor is used, subsequently calling any non-const method
# will trigger the COW mechanism, i.e. a copy is made under the hood.
if QT_LIB == 'PyQt5':
# PyQt5 -> non-const constructor
img_ptr = imgData.ctypes.data
elif QT_LIB == 'PyQt6':
# PyQt5 -> const constructor
# PyQt6 -> non-const constructor
img_ptr = Qt.sip.voidptr(imgData)
else:
## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was)
## So we first attempt the 4.9.6 API, then fall back to 4.9.3
try:
img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat)
except:
if copy:
# does not leak memory, is not mutable
img = QtGui.QImage(buffer(imgData), imgData.shape[1], imgData.shape[0], imgFormat)
else:
# mutable, but leaks memory
img = QtGui.QImage(memoryview(imgData), imgData.shape[1], imgData.shape[0], imgFormat)
# bindings that support ndarray
# PyQt5 -> const constructor
# PySide2 -> non-const constructor
# PySide6 -> non-const constructor
img_ptr = imgData
img = QtGui.QImage(img_ptr, imgData.shape[1], imgData.shape[0], imgFormat)
img.data = imgData
return img
@ -1287,12 +1293,16 @@ def imageToArray(img, copy=False, transpose=True):
if QT_LIB in ['PySide', 'PySide2', 'PySide6']:
arr = np.frombuffer(ptr, dtype=np.ubyte)
else:
ptr.setsize(img.byteCount())
try:
# removed in Qt6
nbytes = img.byteCount()
except AttributeError:
# introduced in Qt 5.10
# however Python 3.7 + PyQt5-5.12 in the CI fails with
# "TypeError: QImage.sizeInBytes() is a private method"
nbytes = img.sizeInBytes()
ptr.setsize(nbytes)
arr = np.asarray(ptr)
if img.byteCount() != arr.size * arr.itemsize:
# Required for Python 2.6, PyQt 4.10
# If this works on all platforms, then there is no need to use np.asarray..
arr = np.frombuffer(ptr, np.ubyte, img.byteCount())
arr = arr.reshape(img.height(), img.width(), 4)
if fmt == img.Format_RGB32:
@ -1546,6 +1556,9 @@ def arrayToQPath(x, y, connect='all'):
buf = QtCore.QByteArray.fromRawData(path.strn)
except TypeError:
buf = QtCore.QByteArray(bytes(path.strn))
except AttributeError:
# PyQt6 raises AttributeError
buf = QtCore.QByteArray(path.strn, path.strn.nbytes)
ds = QtCore.QDataStream(buf)
ds >> path