PythonQwt 0.16.1__tar.gz → 0.16.3__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.3}/PKG-INFO +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.3/PythonQwt.egg-info}/PKG-INFO +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/PythonQwt.egg-info/SOURCES.txt +8 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/index.rst +3 -2
- pythonqwt-0.16.3/doc/reference/qtdesigner.rst +50 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/pyproject.toml +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/__init__.py +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/null_paintdevice.py +1 -1
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot.py +60 -17
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_canvas.py +16 -4
- pythonqwt-0.16.3/qwt/qtdesigner.py +211 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/scale_draw.py +1 -1
- pythonqwt-0.16.3/qwt/tests/test_issue107_gdi_leak.py +72 -0
- pythonqwt-0.16.3/qwt/tests/test_issue109_backingstore.py +68 -0
- pythonqwt-0.16.3/qwt/tests/test_issue33_sidebyside.py +260 -0
- pythonqwt-0.16.3/qwt/tests/test_issue88_mousetracking.py +70 -0
- pythonqwt-0.16.3/qwt/tests/test_qtdesigner.py +55 -0
- pythonqwt-0.16.3/qwt/tests/test_qtdesigner.ui +31 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/utils.py +2 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/text.py +65 -73
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/LICENSE +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/MANIFEST.in +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/PythonQwt-tests.desktop +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/PythonQwt.egg-info/dependency_links.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/PythonQwt.egg-info/entry_points.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/PythonQwt.egg-info/requires.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/PythonQwt.egg-info/top_level.txt +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/README.md +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/_static/PythonQwt_logo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/_static/QwtPlot_example.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/_static/panorama.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/_static/symbol_path_example.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/conf.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/bodedemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/cartesian.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/cpudemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/curvebenchmark1.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/curvebenchmark2.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/curvedemo1.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/curvedemo2.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/data.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/errorbar.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/eventfilter.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/image.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/index.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/logcurve.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/mapdemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/multidemo.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/simple.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/examples/vertical.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/index.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/installation.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/issue93_optimization_summary.md +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/overview.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/plot_example.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/graphic.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/interval.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/plot.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/plot_directpainter.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/plot_layout.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/plot_series.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/scale.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/symbol.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/text.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/toqimage.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/reference/transform.rst +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/doc/symbol_path_example.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/_math.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/color_map.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/column_symbol.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/dyngrid_layout.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/graphic.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/interval.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/legend.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/painter.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/painter_command.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_curve.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_directpainter.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_grid.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_layout.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_marker.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_renderer.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/plot_series.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/qthelpers.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/scale_div.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/scale_engine.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/scale_map.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/scale_widget.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/symbol.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/__init__.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/comparative_benchmarks.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/conftest.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/PythonQwt.svg +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/bodedemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/cartesian.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/cpudemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/curvebenchmark1.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/curvebenchmark2.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/curvedemo1.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/curvedemo2.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/data.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/errorbar.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/eventfilter.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/image.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/loadtest.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/logcurve.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/mapdemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/multidemo.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/simple.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/stylesheet.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/symbol.svg +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/symbols.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/testlauncher.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/data/vertical.png +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_backingstore.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_bodedemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_cartesian.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_cpudemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_curvebenchmark1.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_curvebenchmark2.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_curvedemo1.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_curvedemo2.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_data.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_errorbar.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_eventfilter.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_highdpi.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_image.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_loadtest.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_logcurve.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_mapdemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_multidemo.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_relativemargin.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_simple.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_stylesheet.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_symbols.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/tests/test_vertical.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/toqimage.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/qwt/transform.py +0 -0
- {pythonqwt-0.16.1 → pythonqwt-0.16.3}/setup.cfg +0 -0
|
@@ -44,6 +44,7 @@ doc/reference/plot.rst
|
|
|
44
44
|
doc/reference/plot_directpainter.rst
|
|
45
45
|
doc/reference/plot_layout.rst
|
|
46
46
|
doc/reference/plot_series.rst
|
|
47
|
+
doc/reference/qtdesigner.rst
|
|
47
48
|
doc/reference/scale.rst
|
|
48
49
|
doc/reference/symbol.rst
|
|
49
50
|
doc/reference/text.rst
|
|
@@ -69,6 +70,7 @@ qwt/plot_layout.py
|
|
|
69
70
|
qwt/plot_marker.py
|
|
70
71
|
qwt/plot_renderer.py
|
|
71
72
|
qwt/plot_series.py
|
|
73
|
+
qwt/qtdesigner.py
|
|
72
74
|
qwt/qthelpers.py
|
|
73
75
|
qwt/scale_div.py
|
|
74
76
|
qwt/scale_draw.py
|
|
@@ -95,10 +97,16 @@ qwt/tests/test_errorbar.py
|
|
|
95
97
|
qwt/tests/test_eventfilter.py
|
|
96
98
|
qwt/tests/test_highdpi.py
|
|
97
99
|
qwt/tests/test_image.py
|
|
100
|
+
qwt/tests/test_issue107_gdi_leak.py
|
|
101
|
+
qwt/tests/test_issue109_backingstore.py
|
|
102
|
+
qwt/tests/test_issue33_sidebyside.py
|
|
103
|
+
qwt/tests/test_issue88_mousetracking.py
|
|
98
104
|
qwt/tests/test_loadtest.py
|
|
99
105
|
qwt/tests/test_logcurve.py
|
|
100
106
|
qwt/tests/test_mapdemo.py
|
|
101
107
|
qwt/tests/test_multidemo.py
|
|
108
|
+
qwt/tests/test_qtdesigner.py
|
|
109
|
+
qwt/tests/test_qtdesigner.ui
|
|
102
110
|
qwt/tests/test_relativemargin.py
|
|
103
111
|
qwt/tests/test_simple.py
|
|
104
112
|
qwt/tests/test_stylesheet.py
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Qt Designer
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
PythonQwt ships a Qt Designer plugin which makes a 2D plotting widget
|
|
5
|
+
(:class:`qwt.qtdesigner.QwtPlotWidget`, a thin :class:`qwt.QwtPlot` subclass)
|
|
6
|
+
available directly inside Qt Designer's widget box, in the "PythonQwt" group.
|
|
7
|
+
|
|
8
|
+
Installing the plugin
|
|
9
|
+
---------------------
|
|
10
|
+
|
|
11
|
+
The plugin file ``qwtplugin.py`` lives in the ``qtdesigner`` directory at the
|
|
12
|
+
root of the PythonQwt source tree. To make it available in Qt Designer, add
|
|
13
|
+
that directory to the ``PYQTDESIGNERPATH`` environment variable before
|
|
14
|
+
starting Qt Designer:
|
|
15
|
+
|
|
16
|
+
.. code-block:: bash
|
|
17
|
+
|
|
18
|
+
# Linux/macOS
|
|
19
|
+
export PYQTDESIGNERPATH=<path to PythonQwt>/qtdesigner
|
|
20
|
+
designer
|
|
21
|
+
|
|
22
|
+
.. code-block:: bat
|
|
23
|
+
|
|
24
|
+
rem Windows
|
|
25
|
+
set PYQTDESIGNERPATH=<path to PythonQwt>\qtdesigner
|
|
26
|
+
designer
|
|
27
|
+
|
|
28
|
+
.. note::
|
|
29
|
+
|
|
30
|
+
Qt Designer custom widget plugins require PyQt5 or PyQt6. PySide6 does not
|
|
31
|
+
expose ``QPyDesignerCustomWidgetPlugin``.
|
|
32
|
+
|
|
33
|
+
Loading a ``.ui`` file at runtime
|
|
34
|
+
---------------------------------
|
|
35
|
+
|
|
36
|
+
The :mod:`qwt.qtdesigner` module provides helper functions to load or compile
|
|
37
|
+
``.ui`` files embedding :class:`qwt.qtdesigner.QwtPlotWidget` widgets:
|
|
38
|
+
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
from qwt.qtdesigner import loadui
|
|
42
|
+
|
|
43
|
+
FormClass = loadui("myform.ui")
|
|
44
|
+
form = FormClass()
|
|
45
|
+
form.plotwidget.setTitle("Loaded from Qt Designer")
|
|
46
|
+
|
|
47
|
+
Reference
|
|
48
|
+
---------
|
|
49
|
+
|
|
50
|
+
.. automodule:: qwt.qtdesigner
|
|
@@ -50,7 +50,7 @@ test = ["pytest", "pytest-xvfb"]
|
|
|
50
50
|
include = ["qwt*"]
|
|
51
51
|
|
|
52
52
|
[tool.setuptools.package-data]
|
|
53
|
-
"*" = ["*.png", "*.svg", "*.mo", "*.cfg", "*.toml"]
|
|
53
|
+
"*" = ["*.png", "*.svg", "*.mo", "*.cfg", "*.toml", "*.ui"]
|
|
54
54
|
|
|
55
55
|
[tool.setuptools.dynamic]
|
|
56
56
|
version = { attr = "qwt.__version__" }
|
|
@@ -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.3"
|
|
67
67
|
QWT_VERSION_STR = "6.1.5"
|
|
68
68
|
|
|
69
69
|
|
|
@@ -275,7 +275,7 @@ class QwtNullPaintDevice(QPaintDevice):
|
|
|
275
275
|
elif deviceMetric == QPaintDevice.PdmDevicePixelRatio:
|
|
276
276
|
value = 1
|
|
277
277
|
elif deviceMetric == QPaintDevice.PdmDevicePixelRatioScaled:
|
|
278
|
-
value =
|
|
278
|
+
value = 0x10000
|
|
279
279
|
else:
|
|
280
280
|
value = super(QwtNullPaintDevice, self).metric(deviceMetric)
|
|
281
281
|
return value
|
|
@@ -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
|
|
@@ -996,9 +997,30 @@ class QwtPlot(QFrame):
|
|
|
996
997
|
if canvas is not None:
|
|
997
998
|
canvas.setParent(self)
|
|
998
999
|
canvas.installEventFilter(self)
|
|
1000
|
+
# Mouse move events occur over the canvas widget, not the plot frame
|
|
1001
|
+
# itself, so the canvas must share the plot's mouse-tracking state
|
|
1002
|
+
# for `QwtPlot.setMouseTracking` to behave as expected.
|
|
1003
|
+
canvas.setMouseTracking(self.hasMouseTracking())
|
|
999
1004
|
if self.isVisible():
|
|
1000
1005
|
canvas.show()
|
|
1001
1006
|
|
|
1007
|
+
def setMouseTracking(self, enable):
|
|
1008
|
+
"""
|
|
1009
|
+
Enable or disable mouse tracking for the plot.
|
|
1010
|
+
|
|
1011
|
+
The plot's drawing area is occupied by the canvas widget, so mouse move
|
|
1012
|
+
events over the plot are received by the canvas rather than the plot
|
|
1013
|
+
itself. This override propagates the mouse-tracking state to the canvas
|
|
1014
|
+
so that enabling tracking on the plot delivers mouse move events even
|
|
1015
|
+
when no mouse button is pressed (see Issue #88).
|
|
1016
|
+
|
|
1017
|
+
:param bool enable: True to enable mouse tracking, False to disable it
|
|
1018
|
+
"""
|
|
1019
|
+
super(QwtPlot, self).setMouseTracking(enable)
|
|
1020
|
+
canvas = self.__data.canvas
|
|
1021
|
+
if canvas is not None:
|
|
1022
|
+
canvas.setMouseTracking(enable)
|
|
1023
|
+
|
|
1002
1024
|
def event(self, event):
|
|
1003
1025
|
if event.type() == QEvent.LayoutRequest:
|
|
1004
1026
|
self.updateLayout()
|
|
@@ -1007,7 +1029,12 @@ class QwtPlot(QFrame):
|
|
|
1007
1029
|
return QFrame.event(self, event)
|
|
1008
1030
|
|
|
1009
1031
|
def eventFilter(self, obj, event):
|
|
1010
|
-
|
|
1032
|
+
try:
|
|
1033
|
+
canvas = self.__data.canvas
|
|
1034
|
+
except AttributeError:
|
|
1035
|
+
# The plot is being destroyed: Python attributes already cleared
|
|
1036
|
+
return QFrame.eventFilter(self, obj, event)
|
|
1037
|
+
if obj is canvas:
|
|
1011
1038
|
if event.type() == QEvent.Resize:
|
|
1012
1039
|
self.updateCanvasMargins()
|
|
1013
1040
|
elif event.type() == 178: # QEvent.ContentsRectChange:
|
|
@@ -1195,7 +1222,12 @@ class QwtPlot(QFrame):
|
|
|
1195
1222
|
"""
|
|
1196
1223
|
:return: Return a minimum size hint
|
|
1197
1224
|
"""
|
|
1198
|
-
|
|
1225
|
+
try:
|
|
1226
|
+
layout = self.__data.layout
|
|
1227
|
+
except AttributeError:
|
|
1228
|
+
# The plot is being destroyed: Python attributes already cleared
|
|
1229
|
+
return QSize()
|
|
1230
|
+
hint = layout.minimumSizeHint(self)
|
|
1199
1231
|
hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth())
|
|
1200
1232
|
return hint
|
|
1201
1233
|
|
|
@@ -1815,16 +1847,23 @@ class QwtPlotItem(object):
|
|
|
1815
1847
|
|
|
1816
1848
|
:py:meth:`detach()`
|
|
1817
1849
|
"""
|
|
1818
|
-
if plot is self.
|
|
1850
|
+
if plot is self.plot():
|
|
1819
1851
|
return
|
|
1820
1852
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1853
|
+
current = self.plot()
|
|
1854
|
+
if current is not None:
|
|
1855
|
+
current.attachItem(self, False)
|
|
1823
1856
|
|
|
1824
|
-
|
|
1857
|
+
# Store the parent plot as a WEAK reference: an item must not keep its
|
|
1858
|
+
# plot (and the plot's GDI-backed child widgets) alive. A strong
|
|
1859
|
+
# back-reference here forms a ``plot <-> item`` reference cycle that can
|
|
1860
|
+
# only be reclaimed by the cyclic garbage collector, which delays the
|
|
1861
|
+
# release of native GDI handles and can lead to GDI-handle exhaustion
|
|
1862
|
+
# on Windows when many plots are created and destroyed.
|
|
1863
|
+
self.__data.plot = None if plot is None else weakref.ref(plot)
|
|
1825
1864
|
|
|
1826
|
-
if
|
|
1827
|
-
|
|
1865
|
+
if plot is not None:
|
|
1866
|
+
plot.attachItem(self, True)
|
|
1828
1867
|
|
|
1829
1868
|
def detach(self):
|
|
1830
1869
|
"""
|
|
@@ -1854,7 +1893,8 @@ class QwtPlotItem(object):
|
|
|
1854
1893
|
"""
|
|
1855
1894
|
:return: attached plot
|
|
1856
1895
|
"""
|
|
1857
|
-
|
|
1896
|
+
ref = self.__data.plot
|
|
1897
|
+
return None if ref is None else ref()
|
|
1858
1898
|
|
|
1859
1899
|
def z(self):
|
|
1860
1900
|
"""
|
|
@@ -1881,11 +1921,12 @@ class QwtPlotItem(object):
|
|
|
1881
1921
|
:py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()`
|
|
1882
1922
|
"""
|
|
1883
1923
|
if self.__data.z != z:
|
|
1884
|
-
|
|
1885
|
-
|
|
1924
|
+
plot = self.plot()
|
|
1925
|
+
if plot is not None:
|
|
1926
|
+
plot.attachItem(self, False)
|
|
1886
1927
|
self.__data.z = z
|
|
1887
|
-
if
|
|
1888
|
-
|
|
1928
|
+
if plot is not None:
|
|
1929
|
+
plot.attachItem(self, True)
|
|
1889
1930
|
self.itemChanged()
|
|
1890
1931
|
|
|
1891
1932
|
def setTitle(self, title):
|
|
@@ -2091,8 +2132,9 @@ class QwtPlotItem(object):
|
|
|
2091
2132
|
|
|
2092
2133
|
:py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()`
|
|
2093
2134
|
"""
|
|
2094
|
-
|
|
2095
|
-
|
|
2135
|
+
plot = self.plot()
|
|
2136
|
+
if plot is not None:
|
|
2137
|
+
plot.autoRefresh()
|
|
2096
2138
|
|
|
2097
2139
|
def legendChanged(self):
|
|
2098
2140
|
"""
|
|
@@ -2102,8 +2144,9 @@ class QwtPlotItem(object):
|
|
|
2102
2144
|
|
|
2103
2145
|
:py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()`
|
|
2104
2146
|
"""
|
|
2105
|
-
|
|
2106
|
-
|
|
2147
|
+
plot = self.plot()
|
|
2148
|
+
if self.testItemAttribute(QwtPlotItem.Legend) and plot is not None:
|
|
2149
|
+
plot.updateLegend(self)
|
|
2107
2150
|
|
|
2108
2151
|
def setAxes(self, xAxis, yAxis):
|
|
2109
2152
|
"""
|
|
@@ -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
|
"""
|
|
@@ -584,7 +591,6 @@ class QwtPlotCanvas(QFrame):
|
|
|
584
591
|
if (
|
|
585
592
|
self.testPaintAttribute(self.BackingStore)
|
|
586
593
|
and self.__data.backingStore is not None
|
|
587
|
-
and not self.__data.backingStore.isNull()
|
|
588
594
|
):
|
|
589
595
|
bs = self.__data.backingStore
|
|
590
596
|
pixelRatio = bs.devicePixelRatio()
|
|
@@ -608,6 +614,10 @@ class QwtPlotCanvas(QFrame):
|
|
|
608
614
|
if self.frameWidth() > 0:
|
|
609
615
|
self.drawBorder(p)
|
|
610
616
|
p.end()
|
|
617
|
+
# Store the regenerated pixmap back: the C++ original uses a
|
|
618
|
+
# reference into the stored pixmap, whereas the rebinding above
|
|
619
|
+
# only updates the local variable.
|
|
620
|
+
self.__data.backingStore = bs
|
|
611
621
|
painter.drawPixmap(0, 0, self.__data.backingStore)
|
|
612
622
|
else:
|
|
613
623
|
if self.testAttribute(Qt.WA_StyledBackground):
|
|
@@ -695,7 +705,9 @@ class QwtPlotCanvas(QFrame):
|
|
|
695
705
|
else:
|
|
696
706
|
# print('**DEBUG: QwtPlotCanvas.drawCanvas')
|
|
697
707
|
painter.setClipRect(self.contentsRect(), Qt.IntersectClip)
|
|
698
|
-
self.plot()
|
|
708
|
+
plot = self.plot()
|
|
709
|
+
if plot is not None:
|
|
710
|
+
plot.drawCanvas(painter)
|
|
699
711
|
painter.restore()
|
|
700
712
|
if withBackground and hackStyledBackground:
|
|
701
713
|
# Now paint the border on top
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms of the Qwt License
|
|
4
|
+
# Copyright (c) 2015 Pierre Raybaut, for the Qt Designer plugin helpers
|
|
5
|
+
# (see LICENSE file for more details)
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Qt Designer
|
|
9
|
+
-----------
|
|
10
|
+
|
|
11
|
+
The :mod:`qwt.qtdesigner` module provides helpers to integrate :mod:`qwt`
|
|
12
|
+
widgets into Qt Designer:
|
|
13
|
+
|
|
14
|
+
* :py:class:`QwtPlotWidget`
|
|
15
|
+
* :py:func:`loadui`
|
|
16
|
+
* :py:func:`compileui`
|
|
17
|
+
* :py:func:`create_qtdesigner_plugin`
|
|
18
|
+
|
|
19
|
+
:class:`QwtPlotWidget` is a thin :class:`qwt.QwtPlot` subclass exposing the
|
|
20
|
+
standard Qt widget constructor (``__init__(self, parent=None)``). It is the
|
|
21
|
+
widget promoted by the Qt Designer plugin, so that the code generated by
|
|
22
|
+
``uic`` (e.g. ``QwtPlotWidget(parent=...)`` with PyQt6) instantiates it
|
|
23
|
+
correctly. :class:`qwt.QwtPlot` itself keeps its historical constructor.
|
|
24
|
+
|
|
25
|
+
.. note::
|
|
26
|
+
|
|
27
|
+
Qt Designer custom widget plugins rely on
|
|
28
|
+
:class:`QtDesigner.QPyDesignerCustomWidgetPlugin`, which is only available
|
|
29
|
+
with PyQt5/PyQt6. PySide6 does not expose this class.
|
|
30
|
+
|
|
31
|
+
.. autoclass:: QwtPlotWidget
|
|
32
|
+
|
|
33
|
+
.. autofunction:: loadui
|
|
34
|
+
|
|
35
|
+
.. autofunction:: compileui
|
|
36
|
+
|
|
37
|
+
.. autofunction:: create_qtdesigner_plugin
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import io
|
|
43
|
+
|
|
44
|
+
from qtpy import QtGui as QG
|
|
45
|
+
from qtpy import uic
|
|
46
|
+
|
|
47
|
+
from qwt.plot import QwtPlot
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QwtPlotWidget(QwtPlot):
|
|
51
|
+
""":class:`qwt.QwtPlot` widget with the standard Qt constructor.
|
|
52
|
+
|
|
53
|
+
This subclass is meant to be promoted in Qt Designer: unlike
|
|
54
|
+
:class:`qwt.QwtPlot` (whose constructor accepts the historical
|
|
55
|
+
``QwtPlot([title], [parent])`` overloads), it exposes the conventional
|
|
56
|
+
``__init__(self, parent=None)`` signature expected by the code generated
|
|
57
|
+
by ``uic`` (notably PyQt6, which uses ``QwtPlotWidget(parent=...)``).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
parent: Parent widget
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, parent=None):
|
|
64
|
+
super().__init__(parent)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def loadui(fname: str, replace_class: str | None = None):
|
|
68
|
+
"""Return Widget or Window class from a Qt Designer ``.ui`` file.
|
|
69
|
+
|
|
70
|
+
When a promoted widget inherits from a class that is not known to the
|
|
71
|
+
``uic`` parser (i.e. not a standard Qt widget), ``loadUiType`` fails to
|
|
72
|
+
resolve the widget hierarchy. Passing the offending base class name as
|
|
73
|
+
``replace_class`` neutralizes it by substituting ``QFrame`` (the base
|
|
74
|
+
class of :class:`qwt.QwtPlot`) before the file is parsed.
|
|
75
|
+
|
|
76
|
+
For a plain :class:`qwt.QwtPlot` widget (which already inherits from
|
|
77
|
+
``QFrame``), no replacement is required and ``replace_class`` can be left
|
|
78
|
+
to ``None``.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
fname: Path to the ``.ui`` file
|
|
82
|
+
replace_class: Base class name to replace by ``QFrame``, or ``None``
|
|
83
|
+
to load the file unchanged
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The generated form class
|
|
87
|
+
"""
|
|
88
|
+
with open(fname) as f:
|
|
89
|
+
uifile_text = f.read()
|
|
90
|
+
if replace_class is not None:
|
|
91
|
+
uifile_text = uifile_text.replace(replace_class, "QFrame")
|
|
92
|
+
ui, base_class = uic.loadUiType(io.StringIO(uifile_text))
|
|
93
|
+
|
|
94
|
+
class Form(base_class, ui):
|
|
95
|
+
"""Form class generated from the ``.ui`` file"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, parent=None):
|
|
98
|
+
super().__init__(parent)
|
|
99
|
+
self.setupUi(self)
|
|
100
|
+
|
|
101
|
+
return Form
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def compileui(fname: str, replace_class: str | None = None) -> None:
|
|
105
|
+
"""Compile a Qt Designer ``.ui`` file into a Python module.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
fname: Path to the ``.ui`` file
|
|
109
|
+
replace_class: Base class name to replace by ``QFrame``, or ``None``
|
|
110
|
+
to compile the file unchanged
|
|
111
|
+
"""
|
|
112
|
+
with open(fname) as f:
|
|
113
|
+
uifile_text = f.read()
|
|
114
|
+
if replace_class is not None:
|
|
115
|
+
uifile_text = uifile_text.replace(replace_class, "QFrame")
|
|
116
|
+
with open(fname.replace(".ui", "_ui.py"), "w") as pyfile:
|
|
117
|
+
uic.compileUi(io.StringIO(uifile_text), pyfile, pyqt3_wrapper=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_qtdesigner_plugin(
|
|
121
|
+
group: str,
|
|
122
|
+
module_name: str,
|
|
123
|
+
class_name: str,
|
|
124
|
+
icon: str | QG.QIcon | None = None,
|
|
125
|
+
tooltip: str = "",
|
|
126
|
+
whatsthis: str = "",
|
|
127
|
+
):
|
|
128
|
+
"""Return a custom Qt Designer plugin class.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
group: Name of the group the widget belongs to in Qt Designer
|
|
132
|
+
module_name: Name of the module where the widget class is defined
|
|
133
|
+
class_name: Name of the widget class
|
|
134
|
+
icon: Icon shown in Qt Designer (path to an image file or
|
|
135
|
+
:class:`QtGui.QIcon` instance)
|
|
136
|
+
tooltip: Tool tip shown in Qt Designer
|
|
137
|
+
whatsthis: "What's this" help text shown in Qt Designer
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A :class:`QtDesigner.QPyDesignerCustomWidgetPlugin` subclass
|
|
141
|
+
|
|
142
|
+
Example::
|
|
143
|
+
|
|
144
|
+
Plugin = create_qtdesigner_plugin(
|
|
145
|
+
group="PythonQwt",
|
|
146
|
+
module_name="qwt.qtdesigner",
|
|
147
|
+
class_name="QwtPlotWidget",
|
|
148
|
+
)
|
|
149
|
+
"""
|
|
150
|
+
from qtpy.QtDesigner import QPyDesignerCustomWidgetPlugin
|
|
151
|
+
|
|
152
|
+
Widget = getattr(__import__(module_name, fromlist=[class_name]), class_name)
|
|
153
|
+
|
|
154
|
+
class CustomWidgetPlugin(QPyDesignerCustomWidgetPlugin):
|
|
155
|
+
"""Qt Designer plugin for the ``%s`` widget""" % class_name
|
|
156
|
+
|
|
157
|
+
def __init__(self, parent=None):
|
|
158
|
+
QPyDesignerCustomWidgetPlugin.__init__(self)
|
|
159
|
+
self.initialized = False
|
|
160
|
+
|
|
161
|
+
def initialize(self, core):
|
|
162
|
+
"""Initialize the plugin"""
|
|
163
|
+
if self.initialized:
|
|
164
|
+
return
|
|
165
|
+
self.initialized = True
|
|
166
|
+
|
|
167
|
+
def isInitialized(self):
|
|
168
|
+
"""Return whether the plugin has been initialized"""
|
|
169
|
+
return self.initialized
|
|
170
|
+
|
|
171
|
+
def createWidget(self, parent):
|
|
172
|
+
"""Return a new instance of the custom widget"""
|
|
173
|
+
return Widget(parent)
|
|
174
|
+
|
|
175
|
+
def name(self):
|
|
176
|
+
"""Return the name of the custom widget class"""
|
|
177
|
+
return class_name
|
|
178
|
+
|
|
179
|
+
def group(self):
|
|
180
|
+
"""Return the name of the group the widget belongs to"""
|
|
181
|
+
return group
|
|
182
|
+
|
|
183
|
+
def icon(self):
|
|
184
|
+
"""Return the icon shown in Qt Designer"""
|
|
185
|
+
if isinstance(icon, QG.QIcon):
|
|
186
|
+
return icon
|
|
187
|
+
elif isinstance(icon, str):
|
|
188
|
+
return QG.QIcon(icon)
|
|
189
|
+
return QG.QIcon()
|
|
190
|
+
|
|
191
|
+
def toolTip(self):
|
|
192
|
+
"""Return the tool tip shown in Qt Designer"""
|
|
193
|
+
return tooltip
|
|
194
|
+
|
|
195
|
+
def whatsThis(self):
|
|
196
|
+
"""Return the "What's this" help text shown in Qt Designer"""
|
|
197
|
+
return whatsthis
|
|
198
|
+
|
|
199
|
+
def isContainer(self):
|
|
200
|
+
"""Return whether the widget is a container"""
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
def domXml(self):
|
|
204
|
+
"""Return the XML used to define the widget in Qt Designer"""
|
|
205
|
+
return f'<widget class="{class_name}" name="{class_name.lower()}" />\n'
|
|
206
|
+
|
|
207
|
+
def includeFile(self):
|
|
208
|
+
"""Return the module name where the widget class is defined"""
|
|
209
|
+
return module_name
|
|
210
|
+
|
|
211
|
+
return CustomWidgetPlugin
|
|
@@ -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()
|