Source code for pyqtgraph.Qt

"""
This module exists to smooth out some of the differences between Qt versions.

* Automatically import Qt lib depending on availability
* Allow you to import QtCore/QtGui from pyqtgraph.Qt without specifying which Qt wrapper
  you want to use.
"""
import importlib
import os
import platform
import sys
from importlib import resources

PYSIDE = 'PySide'
PYSIDE2 = 'PySide2'
PYSIDE6 = 'PySide6'
PYQT4 = 'PyQt4'
PYQT5 = 'PyQt5'
PYQT6 = 'PyQt6'

QT_LIB = os.getenv('PYQTGRAPH_QT_LIB')

if QT_LIB is not None:
    try:
        __import__(QT_LIB)
    except ModuleNotFoundError:
        raise ModuleNotFoundError(f"Environment variable PYQTGRAPH_QT_LIB is set to '{os.getenv('PYQTGRAPH_QT_LIB')}', but no module with this name was found.")

## Automatically determine which Qt package to use (unless specified by
## environment variable).
## This is done by first checking to see whether one of the libraries
## is already imported. If not, then attempt to import in the order
## specified in libOrder.
if QT_LIB is None:
    libOrder = [PYQT6, PYSIDE6, PYQT5, PYSIDE2]

    for lib in libOrder:
        if lib in sys.modules:
            QT_LIB = lib
            break

if QT_LIB is None:
    for lib in libOrder:
        qt = lib + '.QtCore'
        try:
            __import__(qt)
            QT_LIB = lib
            break
        except ImportError:
            pass

if QT_LIB is None:
    raise ImportError("PyQtGraph requires one of PyQt5, PyQt6, PySide2 or PySide6; none of these packages could be imported.")


class FailedImport(object):
    """Used to defer ImportErrors until we are sure the module is needed.
    """
    def __init__(self, err):
        self.err = err
        
    def __getattr__(self, attr):
        raise self.err


def _loadUiType(uiFile):
    QtUiTools = importlib.import_module(QT_LIB + '.QtUiTools')
    return QtUiTools.loadUiType(uiFile)


# For historical reasons, pyqtgraph maintains a Qt4-ish interface back when
# there wasn't a QtWidgets module. This _was_ done by monkey-patching all of
# QtWidgets into the QtGui module. This monkey-patching modifies QtGui at a
# global level.
# To avoid this, we now maintain a local "mirror" of QtCore, QtGui and QtWidgets.
# Thus, when monkey-patching happens later on in this file, they will only affect
# the local modules and not the global modules.

from . import QtCore, QtGui, QtWidgets, compat

if QT_LIB == PYQT5:
    try:
        from PyQt5 import sip
    except ImportError:
        # some Linux distros package it this way (e.g. Ubuntu)
        import sip
    from PyQt5 import uic

    try:
        from PyQt5 import QtSvg
    except ImportError as err:
        QtSvg = FailedImport(err)
    try:
        from PyQt5 import QtTest
    except ImportError as err:
        QtTest = FailedImport(err)

    VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR

elif QT_LIB == PYQT6:
    from PyQt6 import sip, uic

    try:
        from PyQt6 import QtSvg
    except ImportError as err:
        QtSvg = FailedImport(err)
    try:
        from PyQt6 import QtTest
    except ImportError as err:
        QtTest = FailedImport(err)

    VERSION_INFO = 'PyQt6 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR

elif QT_LIB == PYSIDE2:
    try:
        from PySide2 import QtSvg
    except ImportError as err:
        QtSvg = FailedImport(err)
    try:
        from PySide2 import QtTest
    except ImportError as err:
        QtTest = FailedImport(err)

    import PySide2
    import shiboken2 as shiboken
    VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
elif QT_LIB == PYSIDE6:
    try:
        from PySide6 import QtSvg
    except ImportError as err:
        QtSvg = FailedImport(err)
    try:
        from PySide6 import QtTest
    except ImportError as err:
        QtTest = FailedImport(err)

    import PySide6
    import shiboken6 as shiboken
    VERSION_INFO = 'PySide6 ' + PySide6.__version__ + ' Qt ' + QtCore.__version__

else:
    raise ValueError("Invalid Qt lib '%s'" % QT_LIB)



if QT_LIB in [PYQT6, PYSIDE6]:
    # PySide6 incorrectly placed QFileSystemModel inside QtWidgets
    if QT_LIB == PYSIDE6 and hasattr(QtWidgets, 'QFileSystemModel'):
        module = getattr(QtWidgets, "QFileSystemModel")
        setattr(QtGui, "QFileSystemModel", module)

else:
    # Shim Qt5 namespace to match Qt6
    module_whitelist = [
        "QAction",
        "QActionGroup",
        "QFileSystemModel",
        "QShortcut",
        "QUndoCommand",
        "QUndoGroup",
        "QUndoStack",
    ]
    for module in module_whitelist:
        attr = getattr(QtWidgets, module)
        setattr(QtGui, module, attr)


# Common to PySide2 and PySide6
if QT_LIB in [PYSIDE2, PYSIDE6]:
    QtVersion = QtCore.__version__
    QtVersionInfo = QtCore.__version_info__
    loadUiType = _loadUiType
    isQObjectAlive = shiboken.isValid

    compat.wrapinstance = shiboken.wrapInstance
    compat.unwrapinstance = lambda x : shiboken.getCppPointer(x)[0]
    compat.voidptr = shiboken.VoidPtr

# Common to PyQt5 and PyQt6
if QT_LIB in [PYQT5, PYQT6]:
    QtVersion = QtCore.QT_VERSION_STR
    QtVersionInfo = tuple((QtCore.QT_VERSION >> i) & 0xff for i in [16,8,0])

    # PyQt, starting in v5.5, calls qAbort when an exception is raised inside
    # a slot. To maintain backward compatibility (and sanity for interactive
    # users), we install a global exception hook to override this behavior.
    if sys.excepthook == sys.__excepthook__:
        sys_excepthook = sys.excepthook
        def pyqt_qabort_override(*args, **kwds):
            return sys_excepthook(*args, **kwds)
        sys.excepthook = pyqt_qabort_override
    
    def isQObjectAlive(obj):
        return not sip.isdeleted(obj)
    
    loadUiType = uic.loadUiType

    QtCore.Signal = QtCore.pyqtSignal
    QtCore.Slot = QtCore.pyqtSlot

    compat.wrapinstance = sip.wrapinstance
    compat.unwrapinstance = sip.unwrapinstance
    compat.voidptr = sip.voidptr

from . import internals

App = QtWidgets.QApplication
# subclassing QApplication causes segfaults on PySide{2, 6} / Python 3.8.7+

QAPP = None
_pgAppInitialized = False
[docs] def mkQApp(name=None): """ Creates new QApplication or returns current instance if existing. ============== ======================================================== **Arguments:** name (str) Application name, passed to Qt ============== ======================================================== """ global QAPP global _pgAppInitialized QAPP = QtWidgets.QApplication.instance() if QAPP is None: # We do not have an already instantiated QApplication # let's add some sane defaults # enable hidpi handling for Qt5 if QtVersionInfo[0] == 5: QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) QAPP = QtWidgets.QApplication(sys.argv or ["pyqtgraph"]) if QtVersionInfo[0] != 5: # issues with dark mode + windows + qt5 QAPP.setStyle("fusion") # set the application icon # python 3.9 won't take "pyqtgraph.icons.peegee" directly traverse_path = resources.files("pyqtgraph.icons") peegee_traverse_path = traverse_path.joinpath("peegee") # as_file requires I feed in a file from the directory... with resources.as_file( peegee_traverse_path.joinpath("peegee.svg") ) as path: # need the parent directory, not the filepath icon_path = path.parent applicationIcon = QtGui.QIcon() applicationIcon.addFile( os.fsdecode(icon_path / "peegee.svg"), ) for sz in [128, 256, 512]: pathname = os.fsdecode(icon_path / f"peegee_{sz}px.png") applicationIcon.addFile(pathname, QtCore.QSize(sz, sz)) # handles the icon showing up on the windows taskbar if platform.system() == 'Windows': import ctypes my_app_id = "pyqtgraph.Qt.mkQApp" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_app_id) QAPP.setWindowIcon(applicationIcon) if not _pgAppInitialized: _pgAppInitialized = True # determine if dark mode try: # this only works in Qt 6.5+ darkMode = QAPP.styleHints().colorScheme() == QtCore.Qt.ColorScheme.Dark QAPP.styleHints().colorSchemeChanged.connect(_onColorSchemeChange) except AttributeError: palette = QAPP.palette() windowTextLightness = palette.color(QtGui.QPalette.ColorRole.WindowText).lightness() windowLightness = palette.color(QtGui.QPalette.ColorRole.Window).lightness() darkMode = windowTextLightness > windowLightness QAPP.paletteChanged.connect(_onPaletteChange) QAPP.setProperty("darkMode", darkMode) if name is not None: QAPP.setApplicationName(name) return QAPP
def _onPaletteChange(palette): # Attempt to keep darkMode attribute up to date # QEvent.Type.PaletteChanged/ApplicationPaletteChanged will be emitted after # paletteChanged.emit()! # Using API deprecated in Qt 6.0 app = mkQApp() windowTextLightness = palette.color(QtGui.QPalette.ColorRole.WindowText).lightness() windowLightness = palette.color(QtGui.QPalette.ColorRole.Window).lightness() darkMode = windowTextLightness > windowLightness app.setProperty('darkMode', darkMode) def _onColorSchemeChange(colorScheme): # Attempt to keep darkMode attribute up to date # QEvent.Type.PaletteChanged/ApplicationPaletteChanged will be emitted before # QStyleHint().colorSchemeChanged.emit()! # Uses Qt 6.5+ API app = mkQApp() darkMode = colorScheme == QtCore.Qt.ColorScheme.Dark app.setProperty('darkMode', darkMode) # exec() is a builtin function, so we define as exec_() here and rename in pg namespace def exec_(): app = mkQApp() return app.exec() if hasattr(app, 'exec') else app.exec_()