PythonQwt 0.16.1__tar.gz → 0.16.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonqwt-0.16.1/PythonQwt.egg-info → pythonqwt-0.16.2}/PKG-INFO +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.2/PythonQwt.egg-info}/PKG-INFO +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/PythonQwt.egg-info/SOURCES.txt +1 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/__init__.py +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot.py +39 -17
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_canvas.py +12 -3
- pythonqwt-0.16.2/qwt/tests/test_issue107_gdi_leak.py +72 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/text.py +65 -73
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/LICENSE +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/MANIFEST.in +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/PythonQwt-tests.desktop +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/PythonQwt.egg-info/dependency_links.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/PythonQwt.egg-info/entry_points.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/PythonQwt.egg-info/requires.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/PythonQwt.egg-info/top_level.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/README.md +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/_static/PythonQwt_logo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/_static/QwtPlot_example.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/_static/panorama.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/_static/symbol_path_example.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/conf.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/bodedemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/cartesian.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/cpudemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/curvebenchmark1.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/curvebenchmark2.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/curvedemo1.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/curvedemo2.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/data.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/errorbar.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/eventfilter.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/image.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/index.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/logcurve.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/mapdemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/multidemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/simple.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/examples/vertical.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/index.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/installation.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/issue93_optimization_summary.md +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/overview.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/plot_example.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/graphic.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/index.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/interval.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/plot.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/plot_directpainter.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/plot_layout.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/plot_series.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/scale.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/symbol.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/text.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/toqimage.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/reference/transform.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/doc/symbol_path_example.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/pyproject.toml +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/_math.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/color_map.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/column_symbol.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/dyngrid_layout.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/graphic.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/interval.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/legend.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/null_paintdevice.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/painter.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/painter_command.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_curve.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_directpainter.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_grid.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_layout.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_marker.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_renderer.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/plot_series.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/qthelpers.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/scale_div.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/scale_draw.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/scale_engine.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/scale_map.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/scale_widget.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/symbol.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/__init__.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/comparative_benchmarks.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/conftest.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/PythonQwt.svg +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/bodedemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/cartesian.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/cpudemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/curvebenchmark1.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/curvebenchmark2.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/curvedemo1.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/curvedemo2.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/data.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/errorbar.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/eventfilter.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/image.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/loadtest.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/logcurve.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/mapdemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/multidemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/simple.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/stylesheet.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/symbol.svg +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/symbols.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/testlauncher.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/data/vertical.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_backingstore.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_bodedemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_cartesian.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_cpudemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_curvebenchmark1.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_curvebenchmark2.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_curvedemo1.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_curvedemo2.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_data.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_errorbar.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_eventfilter.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_highdpi.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_image.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_loadtest.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_logcurve.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_mapdemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_multidemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_relativemargin.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_simple.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_stylesheet.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_symbols.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/test_vertical.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/tests/utils.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/toqimage.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/qwt/transform.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.2}/setup.cfg +0 -0
|
@@ -63,7 +63,7 @@ from qwt.symbol import QwtSymbol as QSbl # see deprecated section
|
|
|
63
63
|
from qwt.text import QwtText # noqa: F401
|
|
64
64
|
from qwt.toqimage import array_to_qimage as toQImage # noqa: F401
|
|
65
65
|
|
|
66
|
-
__version__ = "0.16.
|
|
66
|
+
__version__ = "0.16.2"
|
|
67
67
|
QWT_VERSION_STR = "6.1.5"
|
|
68
68
|
|
|
69
69
|
|
|
@@ -20,6 +20,7 @@ QwtPlotItem
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import math
|
|
23
|
+
import weakref
|
|
23
24
|
|
|
24
25
|
import numpy as np
|
|
25
26
|
from qtpy.QtCore import QEvent, QObject, QRectF, QSize, Qt, Signal
|
|
@@ -1007,7 +1008,12 @@ class QwtPlot(QFrame):
|
|
|
1007
1008
|
return QFrame.event(self, event)
|
|
1008
1009
|
|
|
1009
1010
|
def eventFilter(self, obj, event):
|
|
1010
|
-
|
|
1011
|
+
try:
|
|
1012
|
+
canvas = self.__data.canvas
|
|
1013
|
+
except AttributeError:
|
|
1014
|
+
# The plot is being destroyed: Python attributes already cleared
|
|
1015
|
+
return QFrame.eventFilter(self, obj, event)
|
|
1016
|
+
if obj is canvas:
|
|
1011
1017
|
if event.type() == QEvent.Resize:
|
|
1012
1018
|
self.updateCanvasMargins()
|
|
1013
1019
|
elif event.type() == 178: # QEvent.ContentsRectChange:
|
|
@@ -1195,7 +1201,12 @@ class QwtPlot(QFrame):
|
|
|
1195
1201
|
"""
|
|
1196
1202
|
:return: Return a minimum size hint
|
|
1197
1203
|
"""
|
|
1198
|
-
|
|
1204
|
+
try:
|
|
1205
|
+
layout = self.__data.layout
|
|
1206
|
+
except AttributeError:
|
|
1207
|
+
# The plot is being destroyed: Python attributes already cleared
|
|
1208
|
+
return QSize()
|
|
1209
|
+
hint = layout.minimumSizeHint(self)
|
|
1199
1210
|
hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth())
|
|
1200
1211
|
return hint
|
|
1201
1212
|
|
|
@@ -1815,16 +1826,23 @@ class QwtPlotItem(object):
|
|
|
1815
1826
|
|
|
1816
1827
|
:py:meth:`detach()`
|
|
1817
1828
|
"""
|
|
1818
|
-
if plot is self.
|
|
1829
|
+
if plot is self.plot():
|
|
1819
1830
|
return
|
|
1820
1831
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1832
|
+
current = self.plot()
|
|
1833
|
+
if current is not None:
|
|
1834
|
+
current.attachItem(self, False)
|
|
1823
1835
|
|
|
1824
|
-
|
|
1836
|
+
# Store the parent plot as a WEAK reference: an item must not keep its
|
|
1837
|
+
# plot (and the plot's GDI-backed child widgets) alive. A strong
|
|
1838
|
+
# back-reference here forms a ``plot <-> item`` reference cycle that can
|
|
1839
|
+
# only be reclaimed by the cyclic garbage collector, which delays the
|
|
1840
|
+
# release of native GDI handles and can lead to GDI-handle exhaustion
|
|
1841
|
+
# on Windows when many plots are created and destroyed.
|
|
1842
|
+
self.__data.plot = None if plot is None else weakref.ref(plot)
|
|
1825
1843
|
|
|
1826
|
-
if
|
|
1827
|
-
|
|
1844
|
+
if plot is not None:
|
|
1845
|
+
plot.attachItem(self, True)
|
|
1828
1846
|
|
|
1829
1847
|
def detach(self):
|
|
1830
1848
|
"""
|
|
@@ -1854,7 +1872,8 @@ class QwtPlotItem(object):
|
|
|
1854
1872
|
"""
|
|
1855
1873
|
:return: attached plot
|
|
1856
1874
|
"""
|
|
1857
|
-
|
|
1875
|
+
ref = self.__data.plot
|
|
1876
|
+
return None if ref is None else ref()
|
|
1858
1877
|
|
|
1859
1878
|
def z(self):
|
|
1860
1879
|
"""
|
|
@@ -1881,11 +1900,12 @@ class QwtPlotItem(object):
|
|
|
1881
1900
|
:py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()`
|
|
1882
1901
|
"""
|
|
1883
1902
|
if self.__data.z != z:
|
|
1884
|
-
|
|
1885
|
-
|
|
1903
|
+
plot = self.plot()
|
|
1904
|
+
if plot is not None:
|
|
1905
|
+
plot.attachItem(self, False)
|
|
1886
1906
|
self.__data.z = z
|
|
1887
|
-
if
|
|
1888
|
-
|
|
1907
|
+
if plot is not None:
|
|
1908
|
+
plot.attachItem(self, True)
|
|
1889
1909
|
self.itemChanged()
|
|
1890
1910
|
|
|
1891
1911
|
def setTitle(self, title):
|
|
@@ -2091,8 +2111,9 @@ class QwtPlotItem(object):
|
|
|
2091
2111
|
|
|
2092
2112
|
:py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()`
|
|
2093
2113
|
"""
|
|
2094
|
-
|
|
2095
|
-
|
|
2114
|
+
plot = self.plot()
|
|
2115
|
+
if plot is not None:
|
|
2116
|
+
plot.autoRefresh()
|
|
2096
2117
|
|
|
2097
2118
|
def legendChanged(self):
|
|
2098
2119
|
"""
|
|
@@ -2102,8 +2123,9 @@ class QwtPlotItem(object):
|
|
|
2102
2123
|
|
|
2103
2124
|
:py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()`
|
|
2104
2125
|
"""
|
|
2105
|
-
|
|
2106
|
-
|
|
2126
|
+
plot = self.plot()
|
|
2127
|
+
if self.testItemAttribute(QwtPlotItem.Legend) and plot is not None:
|
|
2128
|
+
plot.updateLegend(self)
|
|
2107
2129
|
|
|
2108
2130
|
def setAxes(self, xAxis, yAxis):
|
|
2109
2131
|
"""
|
|
@@ -13,6 +13,7 @@ QwtPlotCanvas
|
|
|
13
13
|
:members:
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import weakref
|
|
16
17
|
from collections.abc import Sequence
|
|
17
18
|
|
|
18
19
|
from qtpy.QtCore import QEvent, QObject, QPoint, QPointF, QRect, QRectF, QSize, Qt
|
|
@@ -442,7 +443,12 @@ class QwtPlotCanvas(QFrame):
|
|
|
442
443
|
|
|
443
444
|
def __init__(self, plot=None):
|
|
444
445
|
super(QwtPlotCanvas, self).__init__(plot)
|
|
445
|
-
|
|
446
|
+
# Store the parent plot as a WEAK reference to avoid a
|
|
447
|
+
# ``plot <-> canvas`` reference cycle (the plot already owns the canvas
|
|
448
|
+
# through Qt's parent/child relationship). A strong back-reference here
|
|
449
|
+
# would keep the plot alive until the cyclic garbage collector runs,
|
|
450
|
+
# delaying the release of native GDI handles on Windows.
|
|
451
|
+
self.__plot = None if plot is None else weakref.ref(plot)
|
|
446
452
|
self.setFrameStyle(QFrame.Panel | QFrame.Sunken)
|
|
447
453
|
self.setLineWidth(2)
|
|
448
454
|
self.__data = QwtPlotCanvas_PrivateData()
|
|
@@ -456,7 +462,8 @@ class QwtPlotCanvas(QFrame):
|
|
|
456
462
|
"""
|
|
457
463
|
:return: Parent plot widget
|
|
458
464
|
"""
|
|
459
|
-
|
|
465
|
+
ref = self.__plot
|
|
466
|
+
return None if ref is None else ref()
|
|
460
467
|
|
|
461
468
|
def setPaintAttribute(self, attribute, on=True):
|
|
462
469
|
"""
|
|
@@ -695,7 +702,9 @@ class QwtPlotCanvas(QFrame):
|
|
|
695
702
|
else:
|
|
696
703
|
# print('**DEBUG: QwtPlotCanvas.drawCanvas')
|
|
697
704
|
painter.setClipRect(self.contentsRect(), Qt.IntersectClip)
|
|
698
|
-
self.plot()
|
|
705
|
+
plot = self.plot()
|
|
706
|
+
if plot is not None:
|
|
707
|
+
plot.drawCanvas(painter)
|
|
699
708
|
painter.restore()
|
|
700
709
|
if withBackground and hackStyledBackground:
|
|
701
710
|
# Now paint the border on top
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the Qwt License
|
|
4
|
+
# (see LICENSE file for more details)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Test for issue 107: GDI handle exhaustion on Windows.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
from qtpy import QtCore as QC
|
|
13
|
+
from qtpy import QtWidgets as QW
|
|
14
|
+
|
|
15
|
+
from qwt import QwtPlot, QwtPlotCurve
|
|
16
|
+
from qwt.tests.utils import TestEnvironment
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_stress_cycle():
|
|
20
|
+
"""Run one cycle of plot creation, rendering and destruction."""
|
|
21
|
+
plot = QwtPlot()
|
|
22
|
+
plot.resize(QC.QSize(800, 600))
|
|
23
|
+
plot.show()
|
|
24
|
+
|
|
25
|
+
# Create many curves with different fonts (via titles) to pressure caches
|
|
26
|
+
for i in range(20):
|
|
27
|
+
curve = QwtPlotCurve(f"Curve {i}")
|
|
28
|
+
x = np.linspace(0, 10, 100)
|
|
29
|
+
y = np.sin(x + i / 10.0)
|
|
30
|
+
curve.setData(x, y)
|
|
31
|
+
curve.attach(plot)
|
|
32
|
+
|
|
33
|
+
plot.replot()
|
|
34
|
+
QW.QApplication.processEvents()
|
|
35
|
+
|
|
36
|
+
plot.close()
|
|
37
|
+
plot.deleteLater()
|
|
38
|
+
QW.QApplication.processEvents()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_gdi_leak_stability():
|
|
42
|
+
"""
|
|
43
|
+
Repeatedly create/render/destroy plots to check for GDI leak stability.
|
|
44
|
+
On Windows, without the fix, this would crash after a certain number of cycles
|
|
45
|
+
due to GDI handle exhaustion.
|
|
46
|
+
"""
|
|
47
|
+
env = TestEnvironment()
|
|
48
|
+
if not env.unattended:
|
|
49
|
+
pytest.skip("This test is for CI/unattended mode only")
|
|
50
|
+
|
|
51
|
+
# Close any top-level widgets left alive by previous tests in the shared
|
|
52
|
+
# QApplication. This test aggressively drives ``deleteLater()`` +
|
|
53
|
+
# ``processEvents()`` in-process; processing deferred deletions while stale
|
|
54
|
+
# widgets from earlier tests are still pending triggers a Qt teardown
|
|
55
|
+
# fast-fail (0xC0000409) on Windows. Starting from a clean widget set makes
|
|
56
|
+
# the test robust inside the full suite (it already passes in isolation).
|
|
57
|
+
app = QW.QApplication.instance() or QW.QApplication([])
|
|
58
|
+
for widget in list(app.topLevelWidgets()):
|
|
59
|
+
widget.close()
|
|
60
|
+
widget.deleteLater()
|
|
61
|
+
app.processEvents()
|
|
62
|
+
app.sendPostedEvents(None, QC.QEvent.DeferredDelete)
|
|
63
|
+
app.processEvents()
|
|
64
|
+
|
|
65
|
+
n_cycles = 50
|
|
66
|
+
for i in range(n_cycles):
|
|
67
|
+
run_stress_cycle()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
app = QW.QApplication.instance() or QW.QApplication([])
|
|
72
|
+
test_gdi_leak_stability()
|
|
@@ -46,6 +46,7 @@ QwtRichTextEngine
|
|
|
46
46
|
import math
|
|
47
47
|
import os
|
|
48
48
|
import struct
|
|
49
|
+
from collections import OrderedDict
|
|
49
50
|
|
|
50
51
|
from qtpy.QtCore import QObject, QRectF, QSize, QSizeF, Qt
|
|
51
52
|
from qtpy.QtGui import (
|
|
@@ -208,67 +209,50 @@ class QwtTextEngine(object):
|
|
|
208
209
|
pass
|
|
209
210
|
|
|
210
211
|
|
|
211
|
-
ASCENTCACHE =
|
|
212
|
+
ASCENTCACHE = OrderedDict()
|
|
212
213
|
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
#
|
|
217
|
-
# so both dicts stay tiny.
|
|
218
|
-
#
|
|
219
|
-
# This replaces the previous ``id(font) -> font.key()`` design. Two reasons:
|
|
214
|
+
_FM_CACHE_LIMIT = 256 # max QFontMetrics / QFontMetricsF / ascent entries
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Single-slot, leak-free memo for ``QFont.key()``.
|
|
220
218
|
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
219
|
+
# ``QFont.key()`` is the only correct, binding-stable identity for metrics
|
|
220
|
+
# caching, but it is a relatively costly sip call (~1 us on PyQt5, ~1.6 us on
|
|
221
|
+
# PyQt6) and it is queried once per tick label. The previous implementation
|
|
222
|
+
# memoized it in an ``id(font) -> (font, key)`` dict which kept a *strong
|
|
223
|
+
# reference* to every distinct QFont wrapper it saw (up to 1024). That is a
|
|
224
|
+
# genuine memory / GDI-handle leak that the bounded ``.clear()`` only masked
|
|
225
|
+
# (see the issue #93 segfault regression).
|
|
228
226
|
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _font_tuple_key(font):
|
|
241
|
-
"""Build a hashable tuple identifying the logical font."""
|
|
242
|
-
px = font.pixelSize()
|
|
243
|
-
return (
|
|
244
|
-
font.family(),
|
|
245
|
-
px if px > 0 else font.pointSizeF(),
|
|
246
|
-
font.weight(),
|
|
247
|
-
font.italic(),
|
|
248
|
-
font.stretch(),
|
|
249
|
-
font.styleStrategy(),
|
|
250
|
-
)
|
|
227
|
+
# This single-slot memo keeps at most ONE QFont alive (bounded => not a leak)
|
|
228
|
+
# while still skipping the repeated ``font.key()`` call for the very common
|
|
229
|
+
# case of the same painter font being reused across an entire scale-draw pass.
|
|
230
|
+
# Retaining that one font is precisely what makes the ``id()`` comparison safe
|
|
231
|
+
# against id reuse. On a miss the real ``font.key()`` is recomputed, so the
|
|
232
|
+
# result is always lossless and correct.
|
|
233
|
+
_LAST_FONT = None
|
|
234
|
+
_LAST_FONT_ID = None
|
|
235
|
+
_LAST_FONT_KEY = None
|
|
251
236
|
|
|
252
237
|
|
|
253
238
|
def font_key_cached(font):
|
|
254
|
-
"""Return
|
|
239
|
+
"""Return ``font.key()`` using a single-slot, leak-free memo.
|
|
240
|
+
|
|
241
|
+
The returned value is the genuine ``QFont.key()`` string and is safe to use
|
|
242
|
+
as a dict key for metrics caches. At most one ``QFont`` is ever retained.
|
|
255
243
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
metrics caches (callers in this module always compare by ``==`` only).
|
|
244
|
+
:param QFont font: Font
|
|
245
|
+
:return: ``font.key()``
|
|
259
246
|
"""
|
|
247
|
+
global _LAST_FONT, _LAST_FONT_ID, _LAST_FONT_KEY
|
|
260
248
|
fid = id(font)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if len(_FONT_KEY_CACHE) >= _FONT_KEY_CACHE_LIMIT:
|
|
269
|
-
_FONT_KEY_CACHE.clear()
|
|
270
|
-
_FONT_KEY_CACHE[fid] = (font, interned)
|
|
271
|
-
return interned
|
|
249
|
+
if fid == _LAST_FONT_ID:
|
|
250
|
+
return _LAST_FONT_KEY
|
|
251
|
+
key = font.key()
|
|
252
|
+
_LAST_FONT = font
|
|
253
|
+
_LAST_FONT_ID = fid
|
|
254
|
+
_LAST_FONT_KEY = key
|
|
255
|
+
return key
|
|
272
256
|
|
|
273
257
|
|
|
274
258
|
def get_screen_resolution():
|
|
@@ -305,13 +289,9 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
305
289
|
|
|
306
290
|
def __init__(self):
|
|
307
291
|
self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
|
|
308
|
-
self._fm_cache =
|
|
309
|
-
self._fm_cache_f =
|
|
310
|
-
self._margins_cache =
|
|
311
|
-
# Fast path: when textMargins is called repeatedly with the same
|
|
312
|
-
# QFont instance, skip the (expensive) font.key() Qt call.
|
|
313
|
-
self._margins_last_id = -1
|
|
314
|
-
self._margins_last_value = None
|
|
292
|
+
self._fm_cache = OrderedDict()
|
|
293
|
+
self._fm_cache_f = OrderedDict()
|
|
294
|
+
self._margins_cache = OrderedDict()
|
|
315
295
|
|
|
316
296
|
def fontmetrics(self, font):
|
|
317
297
|
fid = font_key_cached(font)
|
|
@@ -319,8 +299,10 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
319
299
|
return self._fm_cache[fid]
|
|
320
300
|
except KeyError:
|
|
321
301
|
if len(self._fm_cache) >= _FM_CACHE_LIMIT:
|
|
322
|
-
self._fm_cache.
|
|
323
|
-
|
|
302
|
+
self._fm_cache.popitem(last=False)
|
|
303
|
+
fm = QFontMetrics(font)
|
|
304
|
+
self._fm_cache[fid] = fm
|
|
305
|
+
return fm
|
|
324
306
|
|
|
325
307
|
def fontmetrics_f(self, font):
|
|
326
308
|
fid = font_key_cached(font)
|
|
@@ -328,8 +310,10 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
328
310
|
return self._fm_cache_f[fid]
|
|
329
311
|
except KeyError:
|
|
330
312
|
if len(self._fm_cache_f) >= _FM_CACHE_LIMIT:
|
|
331
|
-
self._fm_cache_f.
|
|
332
|
-
|
|
313
|
+
self._fm_cache_f.popitem(last=False)
|
|
314
|
+
fm = QFontMetricsF(font)
|
|
315
|
+
self._fm_cache_f[fid] = fm
|
|
316
|
+
return fm
|
|
333
317
|
|
|
334
318
|
def heightForWidth(self, font, flags, text, width):
|
|
335
319
|
"""
|
|
@@ -359,14 +343,15 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
359
343
|
return rect.size()
|
|
360
344
|
|
|
361
345
|
def effectiveAscent(self, font):
|
|
362
|
-
global ASCENTCACHE
|
|
363
346
|
fontKey = font_key_cached(font)
|
|
364
347
|
ascent = ASCENTCACHE.get(fontKey)
|
|
365
348
|
if ascent is not None:
|
|
366
349
|
return ascent
|
|
367
350
|
if len(ASCENTCACHE) >= _FM_CACHE_LIMIT:
|
|
368
|
-
ASCENTCACHE.
|
|
369
|
-
|
|
351
|
+
ASCENTCACHE.popitem(last=False)
|
|
352
|
+
ascent = self.findAscent(font)
|
|
353
|
+
ASCENTCACHE[fontKey] = ascent
|
|
354
|
+
return ascent
|
|
370
355
|
|
|
371
356
|
def findAscent(self, font):
|
|
372
357
|
dummy = "E"
|
|
@@ -409,20 +394,14 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
409
394
|
:param QFont font: Font of the text
|
|
410
395
|
:return: tuple (left, right, top, bottom) representing margins
|
|
411
396
|
"""
|
|
412
|
-
# Fast path: same QFont object as the previous call.
|
|
413
|
-
font_id = id(font)
|
|
414
|
-
if font_id == self._margins_last_id:
|
|
415
|
-
return self._margins_last_value
|
|
416
397
|
fkey = font_key_cached(font)
|
|
417
398
|
cached = self._margins_cache.get(fkey)
|
|
418
399
|
if cached is None:
|
|
419
400
|
fm = self.fontmetrics(font)
|
|
420
401
|
cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent())
|
|
421
402
|
if len(self._margins_cache) >= _FM_CACHE_LIMIT:
|
|
422
|
-
self._margins_cache.
|
|
403
|
+
self._margins_cache.popitem(last=False)
|
|
423
404
|
self._margins_cache[fkey] = cached
|
|
424
|
-
self._margins_last_id = font_id
|
|
425
|
-
self._margins_last_value = cached
|
|
426
405
|
return cached
|
|
427
406
|
|
|
428
407
|
def draw(self, painter, rect, flags, text):
|
|
@@ -588,12 +567,20 @@ class QwtText_LayoutCache(object):
|
|
|
588
567
|
def __init__(self):
|
|
589
568
|
self.textSize = None
|
|
590
569
|
self.fontKey = None
|
|
570
|
+
# ``id(font)`` fast path for ``QwtText.textSize``. ``font`` keeps a
|
|
571
|
+
# strong reference to the cached font so its id cannot be reused by a
|
|
572
|
+
# different object while cached (guards against a stale-size false hit).
|
|
573
|
+
# This is a per-instance, single-slot cache: it retains at most one
|
|
574
|
+
# QFont per QwtText and is released with the QwtText, so unlike the old
|
|
575
|
+
# unbounded module-level font cache it cannot leak.
|
|
591
576
|
self.fontId = -1
|
|
577
|
+
self.font = None
|
|
592
578
|
|
|
593
579
|
def invalidate(self):
|
|
594
580
|
self.textSize = None
|
|
595
581
|
self.fontKey = None
|
|
596
582
|
self.fontId = -1
|
|
583
|
+
self.font = None
|
|
597
584
|
|
|
598
585
|
|
|
599
586
|
class QwtText(object):
|
|
@@ -1108,6 +1095,10 @@ class QwtText(object):
|
|
|
1108
1095
|
"""
|
|
1109
1096
|
font = self.usedFont(defaultFont)
|
|
1110
1097
|
cache = self.__layoutCache
|
|
1098
|
+
# Fast path: same font *object* as the previous call on this text.
|
|
1099
|
+
# ``cache.font`` keeps that font alive, so the id comparison is safe
|
|
1100
|
+
# against id reuse, and we skip the (relatively costly) ``font.key()``
|
|
1101
|
+
# call performed by ``font_key_cached``.
|
|
1111
1102
|
font_id = id(font)
|
|
1112
1103
|
if cache.textSize is not None and cache.fontId == font_id:
|
|
1113
1104
|
sz = QSizeF(cache.textSize)
|
|
@@ -1123,6 +1114,7 @@ class QwtText(object):
|
|
|
1123
1114
|
)
|
|
1124
1115
|
cache.fontKey = fkey
|
|
1125
1116
|
cache.fontId = font_id
|
|
1117
|
+
cache.font = font
|
|
1126
1118
|
sz = QSizeF(cache.textSize)
|
|
1127
1119
|
if self.__data.layoutAttributes & self.MinimumLayout:
|
|
1128
1120
|
(left, right, top, bottom) = self.__data.textEngine.textMargins(font)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|