From 41bfe1e58aefd8863f34ee32cbfa5bd71d6e8b23 Mon Sep 17 00:00:00 2001 From: Duy Anh Philippe PHAM Date: Wed, 10 Jun 2026 15:46:11 +0200 Subject: [PATCH 1/3] Fix: Restore QObject on PrivateData to fix segfault while keeping caching optimizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] Restore QObject as base class for all three PrivateData classes: - QwtText_PrivateData - QwtAbstractScaleDraw_PrivateData - QwtScaleDraw_PrivateData → resolves segfault during lifecycle / teardown --- qwt/scale_draw.py | 33 +++++++-------------------------- qwt/text.py | 21 +++------------------ 2 files changed, 10 insertions(+), 44 deletions(-) diff --git a/qwt/scale_draw.py b/qwt/scale_draw.py index dd049ac..ae09097 100644 --- a/qwt/scale_draw.py +++ b/qwt/scale_draw.py @@ -24,6 +24,7 @@ from qtpy.QtCore import ( QLineF, + QObject, QPoint, QPointF, QRect, @@ -50,22 +51,11 @@ _ALIGN_BOTTOM = int(Qt.AlignBottom) -class QwtAbstractScaleDraw_PrivateData(object): - # See QwtText_PrivateData: ``QObject`` inheritance is unused and the - # base class' ``__init__`` is a measurable cost in tick-heavy renders. - __slots__ = ( - "spacing", - "penWidth", - "minExtent", - "components", - "tick_length", - "tick_lighter_factor", - "map", - "scaleDiv", - "labelCache", - ) +class QwtAbstractScaleDraw_PrivateData(QObject): + # QObject base class restored for Qt parent/child ownership semantics. def __init__(self): + QObject.__init__(self) self.spacing = 4 self.penWidth = 0 self.minExtent = 0.0 @@ -492,20 +482,11 @@ def invalidateCache(self): self.__data.labelCache.clear() -class QwtScaleDraw_PrivateData(object): - # See QwtText_PrivateData: ``QObject`` inheritance is unused and the - # base class' ``__init__`` is a measurable cost in tick-heavy renders. - __slots__ = ( - "len", - "alignment", - "orientation", - "labelAlignment", - "labelRotation", - "labelAutoSize", - "pos", - ) +class QwtScaleDraw_PrivateData(QObject): + # QObject base class restored for Qt parent/child ownership semantics. def __init__(self): + QObject.__init__(self) self.len = 0 self.alignment = QwtScaleDraw.BottomScale # Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment`` diff --git a/qwt/text.py b/qwt/text.py index 2fb0f9a..d41a0c3 100644 --- a/qwt/text.py +++ b/qwt/text.py @@ -557,26 +557,11 @@ def textMargins(self, font): return 0, 0, 0, 0 -class QwtText_PrivateData(object): - # ``QObject`` was previously used as the base class but no Qt signals - # or events are ever emitted from ``_PrivateData`` containers and the - # ``QObject.__init__`` call dominates ``QwtText.__init__`` (it is the - # single most expensive line for tick-label-heavy renders, see - # https://github.com/PlotPyStack/PythonQwt/issues/93). - __slots__ = ( - "renderFlags", - "borderRadius", - "borderPen", - "backgroundBrush", - "paintAttributes", - "layoutAttributes", - "textEngine", - "text", - "font", - "color", - ) +class QwtText_PrivateData(QObject): + # QObject base class restored for Qt parent/child ownership semantics. def __init__(self): + QObject.__init__(self) self.renderFlags = Qt.AlignCenter self.borderRadius = 0 self.borderPen = Qt.NoPen From d57df9817cb4d5c4af48f1b46b49ff28ba8acfb9 Mon Sep 17 00:00:00 2001 From: Duy Anh Philippe PHAM Date: Wed, 10 Jun 2026 16:32:54 +0200 Subject: [PATCH 2/3] Fix cache overflow --- qwt/text.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qwt/text.py b/qwt/text.py index d41a0c3..f39ac56 100644 --- a/qwt/text.py +++ b/qwt/text.py @@ -234,6 +234,7 @@ def draw(self, painter, rect, flags, text): _FONT_TUPLE_CACHE: dict = {} # tuple_key -> tuple_key (interning, also acts # as the "have we seen this logical font" set) _FONT_KEY_CACHE_LIMIT = 1024 +_FM_CACHE_LIMIT = 256 # max QFontMetrics/QFontMetricsF entries per engine def _font_tuple_key(font): @@ -317,6 +318,8 @@ def fontmetrics(self, font): try: return self._fm_cache[fid] except KeyError: + if len(self._fm_cache) >= _FM_CACHE_LIMIT: + self._fm_cache.clear() return self._fm_cache.setdefault(fid, QFontMetrics(font)) def fontmetrics_f(self, font): @@ -324,6 +327,8 @@ def fontmetrics_f(self, font): try: return self._fm_cache_f[fid] except KeyError: + if len(self._fm_cache_f) >= _FM_CACHE_LIMIT: + self._fm_cache_f.clear() return self._fm_cache_f.setdefault(fid, QFontMetricsF(font)) def heightForWidth(self, font, flags, text, width): @@ -359,6 +364,8 @@ def effectiveAscent(self, font): ascent = ASCENTCACHE.get(fontKey) if ascent is not None: return ascent + if len(ASCENTCACHE) >= _FM_CACHE_LIMIT: + ASCENTCACHE.clear() return ASCENTCACHE.setdefault(fontKey, self.findAscent(font)) def findAscent(self, font): @@ -411,6 +418,8 @@ def textMargins(self, font): if cached is None: fm = self.fontmetrics(font) cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent()) + if len(self._margins_cache) >= _FM_CACHE_LIMIT: + self._margins_cache.clear() self._margins_cache[fkey] = cached self._margins_last_id = font_id self._margins_last_value = cached From 3d847f624b1ba3d9172ff668a5be329b763c5b3e Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Mon, 15 Jun 2026 13:52:50 +0200 Subject: [PATCH 3/3] Bump version to 0.16.1 and update changelog --- CHANGELOG.md | 8 ++++++++ qwt/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9de2d8..dd4b694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # PythonQwt Releases +## Version 0.16.1 + +### Bug fixes + +- Fixed [Issue #107](https://github.com/PlotPyStack/PythonQwt/issues/107): corrected a Windows crash/access violation that could occur in long-running sessions creating and rendering large amounts of data, due to GDI handle exhaustion caused by object lifetime and cache growth +- Preserved the rendering performance improvements introduced in 0.16.0 while restoring safer Qt object ownership for internal text/scale-draw private data and adding defensive limits to font-metrics caches + + ## Version 0.16.0 ### Performance diff --git a/qwt/__init__.py b/qwt/__init__.py index 8f0c9ed..72ade3b 100644 --- a/qwt/__init__.py +++ b/qwt/__init__.py @@ -63,7 +63,7 @@ from qwt.text import QwtText # noqa: F401 from qwt.toqimage import array_to_qimage as toQImage # noqa: F401 -__version__ = "0.16.0" +__version__ = "0.16.1" QWT_VERSION_STR = "6.1.5"