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 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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user