1# -*- coding: utf-8 -*- 2""" 3This module exists to smooth out some of the differences between PySide and PyQt4: 4 5* Automatically import either PyQt4 or PySide depending on availability 6* Allow to import QtCore/QtGui pyqtgraph.Qt without specifying which Qt wrapper 7 you want to use. 8* Declare QtCore.Signal, .Slot in PyQt4 9* Declare loadUiType function for Pyside 10 11""" 12 13import os, sys, re, time, subprocess, warnings 14 15 16PYSIDE = 'PySide' 17PYSIDE2 = 'PySide2' 18PYSIDE6 = 'PySide6' 19PYQT4 = 'PyQt4' 20PYQT5 = 'PyQt5' 21PYQT6 = 'PyQt6' 22 23QT_LIB = os.getenv('PYQTGRAPH_QT_LIB') 24 25## Automatically determine which Qt package to use (unless specified by 26## environment variable). 27## This is done by first checking to see whether one of the libraries 28## is already imported. If not, then attempt to import in the order 29## specified in libOrder. 30if QT_LIB is None: 31 libOrder = [PYQT5, PYSIDE2, PYSIDE6, PYQT6] 32 33 for lib in libOrder: 34 if lib in sys.modules: 35 QT_LIB = lib 36 break 37 38if QT_LIB is None: 39 for lib in libOrder: 40 try: 41 __import__(lib) 42 QT_LIB = lib 43 break 44 except ImportError: 45 pass 46 47if QT_LIB is None: 48 raise Exception("PyQtGraph requires one of PyQt5, PyQt6, PySide2 or PySide6; none of these packages could be imported.") 49 50 51class FailedImport(object): 52 """Used to defer ImportErrors until we are sure the module is needed. 53 """ 54 def __init__(self, err): 55 self.err = err 56 57 def __getattr__(self, attr): 58 raise self.err 59 60 61# Make a loadUiType function like PyQt has 62 63# Credit: 64# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 65 66class _StringIO(object): 67 """Alternative to built-in StringIO needed to circumvent unicode/ascii issues""" 68 def __init__(self): 69 self.data = [] 70 71 def write(self, data): 72 self.data.append(data) 73 74 def getvalue(self): 75 return ''.join(map(str, self.data)).encode('utf8') 76 77 78def _loadUiType(uiFile): 79 """ 80 PySide lacks a "loadUiType" command like PyQt4's, so we have to convert 81 the ui file to py code in-memory first and then execute it in a 82 special frame to retrieve the form_class. 83 84 from stackoverflow: http://stackoverflow.com/a/14195313/3781327 85 86 seems like this might also be a legitimate solution, but I'm not sure 87 how to make PyQt4 and pyside look the same... 88 http://stackoverflow.com/a/8717832 89 """ 90 91 pyside2uic = None 92 if QT_LIB == PYSIDE2: 93 try: 94 import pyside2uic 95 except ImportError: 96 # later versions of pyside2 have dropped pyside2uic; use the uic binary instead. 97 pyside2uic = None 98 99 if pyside2uic is None: 100 pyside2version = tuple(map(int, PySide2.__version__.split("."))) 101 if (5, 14) <= pyside2version < (5, 14, 2, 2): 102 warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15') 103 104 # get class names from ui file 105 import xml.etree.ElementTree as xml 106 parsed = xml.parse(uiFile) 107 widget_class = parsed.find('widget').get('class') 108 form_class = parsed.find('class').text 109 110 # convert ui file to python code 111 if pyside2uic is None: 112 uic_executable = QT_LIB.lower() + '-uic' 113 uipy = subprocess.check_output([uic_executable, uiFile]) 114 else: 115 o = _StringIO() 116 with open(uiFile, 'r') as f: 117 pyside2uic.compileUi(f, o, indent=0) 118 uipy = o.getvalue() 119 120 # execute python code 121 pyc = compile(uipy, '<string>', 'exec') 122 frame = {} 123 exec(pyc, frame) 124 125 # fetch the base_class and form class based on their type in the xml from designer 126 form_class = frame['Ui_%s'%form_class] 127 base_class = eval('QtGui.%s'%widget_class) 128 129 return form_class, base_class 130 131 132# For historical reasons, pyqtgraph maintains a Qt4-ish interface back when 133# there wasn't a QtWidgets module. This _was_ done by monkey-patching all of 134# QtWidgets into the QtGui module. This monkey-patching modifies QtGui at a 135# global level. 136# To avoid this, we now maintain a local "mirror" of QtCore, QtGui and QtWidgets. 137# Thus, when monkey-patching happens later on in this file, they will only affect 138# the local modules and not the global modules. 139def _copy_attrs(src, dst): 140 for o in dir(src): 141 if not hasattr(dst, o): 142 setattr(dst, o, getattr(src, o)) 143 144from . import QtCore, QtGui, QtWidgets 145 146if QT_LIB == PYQT5: 147 # We're using PyQt5 which has a different structure so we're going to use a shim to 148 # recreate the Qt4 structure for Qt5 149 import PyQt5.QtCore, PyQt5.QtGui, PyQt5.QtWidgets 150 _copy_attrs(PyQt5.QtCore, QtCore) 151 _copy_attrs(PyQt5.QtGui, QtGui) 152 _copy_attrs(PyQt5.QtWidgets, QtWidgets) 153 154 try: 155 from PyQt5 import sip 156 except ImportError: 157 # some Linux distros package it this way (e.g. Ubuntu) 158 import sip 159 from PyQt5 import uic 160 161 try: 162 from PyQt5 import QtSvg 163 except ImportError as err: 164 QtSvg = FailedImport(err) 165 try: 166 from PyQt5 import QtTest 167 except ImportError as err: 168 QtTest = FailedImport(err) 169 170 VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR 171 172elif QT_LIB == PYQT6: 173 import PyQt6.QtCore, PyQt6.QtGui, PyQt6.QtWidgets 174 _copy_attrs(PyQt6.QtCore, QtCore) 175 _copy_attrs(PyQt6.QtGui, QtGui) 176 _copy_attrs(PyQt6.QtWidgets, QtWidgets) 177 178 from PyQt6 import sip, uic 179 180 try: 181 from PyQt6 import QtSvg 182 except ImportError as err: 183 QtSvg = FailedImport(err) 184 try: 185 from PyQt6 import QtOpenGLWidgets 186 except ImportError as err: 187 QtOpenGLWidgets = FailedImport(err) 188 try: 189 from PyQt6 import QtTest 190 except ImportError as err: 191 QtTest = FailedImport(err) 192 193 VERSION_INFO = 'PyQt6 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR 194 195elif QT_LIB == PYSIDE2: 196 import PySide2.QtCore, PySide2.QtGui, PySide2.QtWidgets 197 _copy_attrs(PySide2.QtCore, QtCore) 198 _copy_attrs(PySide2.QtGui, QtGui) 199 _copy_attrs(PySide2.QtWidgets, QtWidgets) 200 201 try: 202 from PySide2 import QtSvg 203 except ImportError as err: 204 QtSvg = FailedImport(err) 205 try: 206 from PySide2 import QtTest 207 except ImportError as err: 208 QtTest = FailedImport(err) 209 210 import shiboken2 as shiboken 211 import PySide2 212 VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__ 213elif QT_LIB == PYSIDE6: 214 import PySide6.QtCore, PySide6.QtGui, PySide6.QtWidgets 215 _copy_attrs(PySide6.QtCore, QtCore) 216 _copy_attrs(PySide6.QtGui, QtGui) 217 _copy_attrs(PySide6.QtWidgets, QtWidgets) 218 219 try: 220 from PySide6 import QtSvg 221 except ImportError as err: 222 QtSvg = FailedImport(err) 223 try: 224 from PySide6 import QtOpenGLWidgets 225 except ImportError as err: 226 QtOpenGLWidgets = FailedImport(err) 227 try: 228 from PySide6 import QtTest 229 except ImportError as err: 230 QtTest = FailedImport(err) 231 232 import shiboken6 as shiboken 233 import PySide6 234 VERSION_INFO = 'PySide6 ' + PySide6.__version__ + ' Qt ' + QtCore.__version__ 235 236else: 237 raise ValueError("Invalid Qt lib '%s'" % QT_LIB) 238 239 240# common to PyQt5, PyQt6, PySide2 and PySide6 241if QT_LIB in [PYQT5, PYQT6, PYSIDE2, PYSIDE6]: 242 # We're using Qt5 which has a different structure so we're going to use a shim to 243 # recreate the Qt4 structure 244 245 if QT_LIB in [PYQT5, PYSIDE2]: 246 __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale 247 248 def scale(self, *args): 249 warnings.warn( 250 "Deprecated Qt API, will be removed in 0.13.0.", 251 DeprecationWarning, stacklevel=2 252 ) 253 if args: 254 sx, sy = args 255 tr = self.transform() 256 tr.scale(sx, sy) 257 self.setTransform(tr) 258 else: 259 return __QGraphicsItem_scale(self) 260 QtWidgets.QGraphicsItem.scale = scale 261 262 def rotate(self, angle): 263 warnings.warn( 264 "Deprecated Qt API, will be removed in 0.13.0.", 265 DeprecationWarning, stacklevel=2 266 ) 267 tr = self.transform() 268 tr.rotate(angle) 269 self.setTransform(tr) 270 QtWidgets.QGraphicsItem.rotate = rotate 271 272 def translate(self, dx, dy): 273 warnings.warn( 274 "Deprecated Qt API, will be removed in 0.13.0.", 275 DeprecationWarning, stacklevel=2 276 ) 277 tr = self.transform() 278 tr.translate(dx, dy) 279 self.setTransform(tr) 280 QtWidgets.QGraphicsItem.translate = translate 281 282 def setMargin(self, i): 283 warnings.warn( 284 "Deprecated Qt API, will be removed in 0.13.0.", 285 DeprecationWarning, stacklevel=2 286 ) 287 self.setContentsMargins(i, i, i, i) 288 QtWidgets.QGridLayout.setMargin = setMargin 289 290 def setResizeMode(self, *args): 291 warnings.warn( 292 "Deprecated Qt API, will be removed in 0.13.0.", 293 DeprecationWarning, stacklevel=2 294 ) 295 self.setSectionResizeMode(*args) 296 QtWidgets.QHeaderView.setResizeMode = setResizeMode 297 298 # Import all QtWidgets objects into QtGui 299 for o in dir(QtWidgets): 300 if o.startswith('Q'): 301 setattr(QtGui, o, getattr(QtWidgets,o) ) 302 303 QtGui.QApplication.setGraphicsSystem = None 304 305 306if QT_LIB in [PYQT6, PYSIDE6]: 307 # We're using Qt6 which has a different structure so we're going to use a shim to 308 # recreate the Qt5 structure 309 310 if not isinstance(QtOpenGLWidgets, FailedImport): 311 QtWidgets.QOpenGLWidget = QtOpenGLWidgets.QOpenGLWidget 312 313 314# Common to PySide2 and PySide6 315if QT_LIB in [PYSIDE2, PYSIDE6]: 316 QtVersion = QtCore.__version__ 317 loadUiType = _loadUiType 318 isQObjectAlive = shiboken.isValid 319 320 # PySide does not implement qWait 321 if not isinstance(QtTest, FailedImport): 322 if not hasattr(QtTest.QTest, 'qWait'): 323 @staticmethod 324 def qWait(msec): 325 start = time.time() 326 QtGui.QApplication.processEvents() 327 while time.time() < start + msec * 0.001: 328 QtGui.QApplication.processEvents() 329 QtTest.QTest.qWait = qWait 330 331 332# Common to PyQt5 and PyQt6 333if QT_LIB in [PYQT5, PYQT6]: 334 QtVersion = QtCore.QT_VERSION_STR 335 336 # PyQt, starting in v5.5, calls qAbort when an exception is raised inside 337 # a slot. To maintain backward compatibility (and sanity for interactive 338 # users), we install a global exception hook to override this behavior. 339 if sys.excepthook == sys.__excepthook__: 340 sys_excepthook = sys.excepthook 341 def pyqt_qabort_override(*args, **kwds): 342 return sys_excepthook(*args, **kwds) 343 sys.excepthook = pyqt_qabort_override 344 345 def isQObjectAlive(obj): 346 return not sip.isdeleted(obj) 347 348 loadUiType = uic.loadUiType 349 350 QtCore.Signal = QtCore.pyqtSignal 351 352# USE_XXX variables are deprecated 353USE_PYSIDE = QT_LIB == PYSIDE 354USE_PYQT4 = QT_LIB == PYQT4 355USE_PYQT5 = QT_LIB == PYQT5 356 357## Make sure we have Qt >= 5.12 358versionReq = [5, 12] 359m = re.match(r'(\d+)\.(\d+).*', QtVersion) 360if m is not None and list(map(int, m.groups())) < versionReq: 361 print(list(map(int, m.groups()))) 362 raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) 363 364App = QtWidgets.QApplication 365# subclassing QApplication causes segfaults on PySide{2, 6} / Python 3.8.7+ 366 367QAPP = None 368def mkQApp(name=None): 369 """ 370 Creates new QApplication or returns current instance if existing. 371 372 ============== ======================================================== 373 **Arguments:** 374 name (str) Application name, passed to Qt 375 ============== ======================================================== 376 """ 377 global QAPP 378 379 def onPaletteChange(palette): 380 color = palette.base().color().name() 381 app = QtWidgets.QApplication.instance() 382 app.setProperty('darkMode', color.lower() != "#ffffff") 383 384 QAPP = QtGui.QApplication.instance() 385 if QAPP is None: 386 # hidpi handling 387 qtVersionCompare = tuple(map(int, QtVersion.split("."))) 388 if qtVersionCompare > (6, 0): 389 # Qt6 seems to support hidpi without needing to do anything so continue 390 pass 391 elif qtVersionCompare > (5, 14): 392 os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" 393 QtGui.QApplication.setHighDpiScaleFactorRoundingPolicy(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) 394 else: # qt 5.12 and 5.13 395 QtGui.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) 396 QtGui.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) 397 QAPP = QtGui.QApplication(sys.argv or ["pyqtgraph"]) 398 QAPP.paletteChanged.connect(onPaletteChange) 399 QAPP.paletteChanged.emit(QAPP.palette()) 400 401 if name is not None: 402 QAPP.setApplicationName(name) 403 return QAPP 404 405 406# exec() is used within _loadUiType, so we define as exec_() here and rename in pg namespace 407def exec_(): 408 app = mkQApp() 409 return app.exec() if hasattr(app, 'exec') else app.exec_() 410