From c9a3eab916d679d43366c33c401c8ac34ee92468 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sun, 1 Feb 2026 12:25:31 +0800 Subject: [PATCH 1/4] drain OpenGL errors triggered by Qt --- pyqtgraph/opengl/GLViewWidget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index b23a3e57d6..51be63a70a 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -211,6 +211,10 @@ def itemsAt(self, region=None): return [self._itemNames[i[1]] for i in items] def paintGL(self): + # Qt may have triggered some OpenGL errors, drain those errors away. + while GL.glGetError() != GL.GL_NO_ERROR: + pass + # when called by Qt, glViewport has already been called # with device pixel ratio taken of region = self.getViewport() From 92096ee2e898029603487619e176a2c9494c0d3f Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 26 Feb 2026 14:34:25 +0800 Subject: [PATCH 2/4] populate QtCore, QtGui, QtWidgets lazily --- pyqtgraph/Qt/QtCore/__init__.py | 8 +++++++ pyqtgraph/Qt/QtGui/__init__.py | 8 +++++++ pyqtgraph/Qt/QtWidgets/__init__.py | 8 +++++++ pyqtgraph/Qt/__init__.py | 34 ------------------------------ 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/pyqtgraph/Qt/QtCore/__init__.py b/pyqtgraph/Qt/QtCore/__init__.py index e69de29bb2..0de152521d 100644 --- a/pyqtgraph/Qt/QtCore/__init__.py +++ b/pyqtgraph/Qt/QtCore/__init__.py @@ -0,0 +1,8 @@ +import importlib +from .. import QT_LIB +module = importlib.import_module(f'{QT_LIB}.QtCore') + +def __getattr__(name): + x = getattr(module, name) + globals()[name] = x + return x diff --git a/pyqtgraph/Qt/QtGui/__init__.py b/pyqtgraph/Qt/QtGui/__init__.py index e69de29bb2..7736c54198 100644 --- a/pyqtgraph/Qt/QtGui/__init__.py +++ b/pyqtgraph/Qt/QtGui/__init__.py @@ -0,0 +1,8 @@ +import importlib +from .. import QT_LIB +module = importlib.import_module(f'{QT_LIB}.QtGui') + +def __getattr__(name): + x = getattr(module, name) + globals()[name] = x + return x diff --git a/pyqtgraph/Qt/QtWidgets/__init__.py b/pyqtgraph/Qt/QtWidgets/__init__.py index e69de29bb2..f01c003867 100644 --- a/pyqtgraph/Qt/QtWidgets/__init__.py +++ b/pyqtgraph/Qt/QtWidgets/__init__.py @@ -0,0 +1,8 @@ +import importlib +from .. import QT_LIB +module = importlib.import_module(f'{QT_LIB}.QtWidgets') + +def __getattr__(name): + x = getattr(module, name) + globals()[name] = x + return x diff --git a/pyqtgraph/Qt/__init__.py b/pyqtgraph/Qt/__init__.py index 2356f1a4f6..42a1c09651 100644 --- a/pyqtgraph/Qt/__init__.py +++ b/pyqtgraph/Qt/__init__.py @@ -110,23 +110,10 @@ def _loadUiType(uiFile): # 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. -def _copy_attrs(src, dst): - for o in dir(src): - if not hasattr(dst, o): - setattr(dst, o, getattr(src, o)) from . import QtCore, QtGui, QtWidgets, compat if QT_LIB == PYQT5: - # We're using PyQt5 which has a different structure so we're going to use a shim to - # recreate the Qt4 structure for Qt5 - import PyQt5.QtCore - import PyQt5.QtGui - import PyQt5.QtWidgets - _copy_attrs(PyQt5.QtCore, QtCore) - _copy_attrs(PyQt5.QtGui, QtGui) - _copy_attrs(PyQt5.QtWidgets, QtWidgets) - try: from PyQt5 import sip except ImportError: @@ -146,13 +133,6 @@ def _copy_attrs(src, dst): VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR elif QT_LIB == PYQT6: - import PyQt6.QtCore - import PyQt6.QtGui - import PyQt6.QtWidgets - _copy_attrs(PyQt6.QtCore, QtCore) - _copy_attrs(PyQt6.QtGui, QtGui) - _copy_attrs(PyQt6.QtWidgets, QtWidgets) - from PyQt6 import sip, uic try: @@ -171,13 +151,6 @@ def _copy_attrs(src, dst): VERSION_INFO = 'PyQt6 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR elif QT_LIB == PYSIDE2: - import PySide2.QtCore - import PySide2.QtGui - import PySide2.QtWidgets - _copy_attrs(PySide2.QtCore, QtCore) - _copy_attrs(PySide2.QtGui, QtGui) - _copy_attrs(PySide2.QtWidgets, QtWidgets) - try: from PySide2 import QtSvg except ImportError as err: @@ -191,13 +164,6 @@ def _copy_attrs(src, dst): import shiboken2 as shiboken VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__ elif QT_LIB == PYSIDE6: - import PySide6.QtCore - import PySide6.QtGui - import PySide6.QtWidgets - _copy_attrs(PySide6.QtCore, QtCore) - _copy_attrs(PySide6.QtGui, QtGui) - _copy_attrs(PySide6.QtWidgets, QtWidgets) - try: from PySide6 import QtSvg except ImportError as err: From bed4b494a513f77e28e442440574b8c27fc5272e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 26 Feb 2026 14:44:11 +0800 Subject: [PATCH 3/4] don't shim QOpenGLWidget into QtWidgets --- pyqtgraph/Qt/__init__.py | 14 -------------- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 +++- pyqtgraph/opengl/GLViewWidget.py | 4 +++- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/Qt/__init__.py b/pyqtgraph/Qt/__init__.py index 42a1c09651..124cbc233c 100644 --- a/pyqtgraph/Qt/__init__.py +++ b/pyqtgraph/Qt/__init__.py @@ -139,10 +139,6 @@ def _loadUiType(uiFile): 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 except ImportError as err: @@ -168,10 +164,6 @@ def _loadUiType(uiFile): from PySide6 import QtSvg except ImportError as err: QtSvg = FailedImport(err) - try: - from PySide6 import QtOpenGLWidgets - except ImportError as err: - QtOpenGLWidgets = FailedImport(err) try: from PySide6 import QtTest except ImportError as err: @@ -187,12 +179,6 @@ def _loadUiType(uiFile): if QT_LIB in [PYQT6, PYSIDE6]: - # We're using Qt6 which has a different structure so we're going to use a shim to - # recreate the Qt5 structure - - if not isinstance(QtOpenGLWidgets, FailedImport): - QtWidgets.QOpenGLWidget = QtOpenGLWidgets.QOpenGLWidget - # PySide6 incorrectly placed QFileSystemModel inside QtWidgets if QT_LIB == PYSIDE6 and hasattr(QtWidgets, 'QFileSystemModel'): module = getattr(QtWidgets, "QFileSystemModel") diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 1b687aa933..223449e68b 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -15,8 +15,10 @@ if QtVersionInfo[0] >= 6: QtOpenGL = importlib.import_module(f"{QT_LIB}.QtOpenGL") + QtOpenGLWidgets = importlib.import_module(f"{QT_LIB}.QtOpenGLWidgets") else: QtOpenGL = QtGui + QtOpenGLWidgets = QtWidgets __all__ = ['PlotCurveItem'] @@ -875,7 +877,7 @@ def _getFillPathList(self, widget): # Note: when OpenGL mode is enabled, we should normally be using the # 'paintGL' method, and should not even reach here. # Values were found using 'PlotSpeedTest.py' example, see #2257. - chunksize = 50 if not isinstance(widget, QtWidgets.QOpenGLWidget) else 5000 + chunksize = 50 if not isinstance(widget, QtOpenGLWidgets.QOpenGLWidget) else 5000 connect_kind = self.opts['connect'] if isinstance(connect_kind, np.ndarray): diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 51be63a70a..4b9314c557 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -12,8 +12,10 @@ if QtVersionInfo[0] >= 6: QtOpenGL = importlib.import_module(f"{QT_LIB}.QtOpenGL") + QtOpenGLWidgets = importlib.import_module(f"{QT_LIB}.QtOpenGLWidgets") else: QtOpenGL = QtGui + QtOpenGLWidgets = QtWidgets class GLViewMixin: def __init__(self, *args, rotationMethod='euler', **kwargs): @@ -551,7 +553,7 @@ def renderToArray(self, size, format=GL.GL_BGRA, type=GL.GL_UNSIGNED_BYTE, textu return output -class GLViewWidget(GLViewMixin, QtWidgets.QOpenGLWidget): +class GLViewWidget(GLViewMixin, QtOpenGLWidgets.QOpenGLWidget): def __init__(self, *args, devicePixelRatio=None, **kwargs): """ Basic widget for displaying 3D data From 1b84c2b2d5b6f22b41c91733bc4ace808d14998c Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 27 Feb 2026 18:52:53 +0800 Subject: [PATCH 4/4] svg path: support Z closepath command --- pyqtgraph/exporters/SVGExporter.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index af07a16ee4..8f7aacb180 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -419,19 +419,25 @@ def correctCoordinates(node, defs, item, options): ch.setAttribute('points', ' '.join([','.join([str(a) for a in c]) for c in coords])) elif ch.tagName == 'path': removeTransform = True - newCoords = '' oldCoords = ch.getAttribute('d').strip() if oldCoords == '': continue + newCoords = [] for c in oldCoords.split(' '): - x,y = c.split(',') - if x[0].isalpha(): - t = x[0] - x = x[1:] + tokens = c.split(',') + if len(tokens) == 1: + # this might be a Z + newCoords.append(tokens[0]) else: - t = '' - nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) - newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' ' + x, y = tokens + if x[0].isalpha(): + t = x[0] + x = x[1:] + else: + t = '' + nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) + newCoords.append(t+str(nc[0,0])+','+str(nc[0,1])) + newCoords = ' '.join(newCoords) # If coords start with L instead of M, then the entire path will not be rendered. # (This can happen if the first point had nan values in it--Qt will skip it on export) if newCoords[0] != 'M':