add PyQt6 support to Qt.py and functions.py
This commit is contained in:
parent
ab41c03358
commit
dcbddb0abf
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user