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 os, sys, re, time, subprocess, warnings
import importlib
import enum
from .python2_3 import asUnicode 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 ## 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. ## is already imported. If not, then attempt to import PyQt4, then PySide.
if QT_LIB is None: if QT_LIB is None:
libOrder = [PYQT4, PYSIDE, PYQT5, PYSIDE2, PYSIDE6] libOrder = [PYQT4, PYSIDE, PYQT5, PYSIDE2, PYSIDE6, PYQT6]
for lib in libOrder: for lib in libOrder:
if lib in sys.modules: if lib in sys.modules:
@ -45,7 +47,7 @@ if QT_LIB is None:
pass pass
if QT_LIB is None: 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): class FailedImport(object):
@ -221,6 +223,25 @@ elif QT_LIB == PYQT5:
VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR 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: elif QT_LIB == PYSIDE2:
from PySide2 import QtGui, QtCore, QtWidgets from PySide2 import QtGui, QtCore, QtWidgets
@ -277,8 +298,8 @@ else:
raise ValueError("Invalid Qt lib '%s'" % QT_LIB) raise ValueError("Invalid Qt lib '%s'" % QT_LIB)
# common to PyQt5, PySide2 and PySide6 # common to PyQt5, PyQt6, PySide2 and PySide6
if QT_LIB in [PYQT5, PYSIDE2, 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 # We're using Qt5 which has a different structure so we're going to use a shim to
# recreate the Qt4 structure # recreate the Qt4 structure
@ -353,13 +374,13 @@ if QT_LIB in [PYSIDE, PYSIDE2, PYSIDE6]:
QtTest.QTest.qWait = qWait QtTest.QTest.qWait = qWait
# Common to PyQt4 and 5 # Common to PyQt4, PyQt5 and PyQt6
if QT_LIB in [PYQT4, PYQT5]: if QT_LIB in [PYQT4, PYQT5, PYQT6]:
QtVersion = QtCore.QT_VERSION_STR QtVersion = QtCore.QT_VERSION_STR
try: try:
from PyQt5 import sip sip = importlib.import_module(QT_LIB + '.sip')
except ImportError: except ModuleNotFoundError:
import sip import sip
def isQObjectAlive(obj): def isQObjectAlive(obj):
return not sip.isdeleted(obj) return not sip.isdeleted(obj)
@ -369,6 +390,58 @@ if QT_LIB in [PYQT4, PYQT5]:
QtCore.Signal = QtCore.pyqtSignal 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_XXX variables are deprecated
USE_PYSIDE = QT_LIB == PYSIDE USE_PYSIDE = QT_LIB == PYSIDE
USE_PYQT4 = QT_LIB == PYQT4 USE_PYQT4 = QT_LIB == PYQT4

View File

@ -19,6 +19,7 @@ from pyqtgraph.util.cupy_helper import getCupy
from . import debug, reload from . import debug, reload
from .Qt import QtGui, QtCore, QT_LIB, QtVersion from .Qt import QtGui, QtCore, QT_LIB, QtVersion
from . import Qt
from .metaarray import MetaArray from .metaarray import MetaArray
from .pgcollections import OrderedDict from .pgcollections import OrderedDict
from .python2_3 import asUnicode, basestring from .python2_3 import asUnicode, basestring
@ -1254,23 +1255,28 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
imgData = imgData.copy() imgData = imgData.copy()
profile("copy") profile("copy")
if QT_LIB == 'PySide':
ch = ctypes.c_char.from_buffer(imgData, 0) # C++ QImage has two kind of constructors
img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) # - QImage(const uchar*, ...)
elif QT_LIB in ['PySide2', 'PySide6']: # - QImage(uchar*, ...)
img = QtGui.QImage(imgData, imgData.shape[1], imgData.shape[0], imgFormat) # 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: else:
## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was) # bindings that support ndarray
## So we first attempt the 4.9.6 API, then fall back to 4.9.3 # PyQt5 -> const constructor
try: # PySide2 -> non-const constructor
img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat) # PySide6 -> non-const constructor
except: img_ptr = imgData
if copy:
# does not leak memory, is not mutable img = QtGui.QImage(img_ptr, imgData.shape[1], imgData.shape[0], imgFormat)
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)
img.data = imgData img.data = imgData
return img return img
@ -1287,12 +1293,16 @@ def imageToArray(img, copy=False, transpose=True):
if QT_LIB in ['PySide', 'PySide2', 'PySide6']: if QT_LIB in ['PySide', 'PySide2', 'PySide6']:
arr = np.frombuffer(ptr, dtype=np.ubyte) arr = np.frombuffer(ptr, dtype=np.ubyte)
else: 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) 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) arr = arr.reshape(img.height(), img.width(), 4)
if fmt == img.Format_RGB32: if fmt == img.Format_RGB32:
@ -1546,6 +1556,9 @@ def arrayToQPath(x, y, connect='all'):
buf = QtCore.QByteArray.fromRawData(path.strn) buf = QtCore.QByteArray.fromRawData(path.strn)
except TypeError: except TypeError:
buf = QtCore.QByteArray(bytes(path.strn)) buf = QtCore.QByteArray(bytes(path.strn))
except AttributeError:
# PyQt6 raises AttributeError
buf = QtCore.QByteArray(path.strn, path.strn.nbytes)
ds = QtCore.QDataStream(buf) ds = QtCore.QDataStream(buf)
ds >> path ds >> path