"""
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_()