PythonQwt 0.16.1__py3-none-any.whl → 0.16.3__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonQwt
3
- Version: 0.16.1
3
+ Version: 0.16.3
4
4
  Summary: Qt plotting widgets for Python
5
5
  Author-email: Pierre Raybaut <pierre.raybaut@gmail.com>
6
6
  License: PythonQwt License Agreement
@@ -1,5 +1,5 @@
1
- pythonqwt-0.16.1.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
2
- qwt/__init__.py,sha256=Aux0XzsSsxiS6O8LFosjbqk7X0rIU4kE3Si0a9jGBaE,5990
1
+ pythonqwt-0.16.3.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
2
+ qwt/__init__.py,sha256=0eKAVij0c3t6sNd8fHi5OulCvZbC3PAT6hZrJcCSgJU,5990
3
3
  qwt/_math.py,sha256=fNcPJcaK-ldCCFB20T2N58LEkQ1lwZY1-q5veXXhwl8,1501
4
4
  qwt/color_map.py,sha256=fLRpymqOXtLrhuPrbbCxTvNINbr2GH7pYBG4fZidrcs,11812
5
5
  qwt/column_symbol.py,sha256=0aFyQ05ryTKbaSTctkhtXa0pB_tWG444zlH3991_bt0,5760
@@ -7,11 +7,11 @@ qwt/dyngrid_layout.py,sha256=wgff6mZ1okNhQRRbwaw1I__vOXqGdUG0uafkShuns08,13477
7
7
  qwt/graphic.py,sha256=rSxvgIAm9ctcyszvZrN_03KpBhKSHabbPYdcrztSta8,28962
8
8
  qwt/interval.py,sha256=PMrCQxV9NXGk2JG0MNjJ3zGTNTSSRfa_noxbpm-Okns,12287
9
9
  qwt/legend.py,sha256=GBUim67HWgwLat5tOWDpK98fWcFxV-RkxWjoyWd9Gao,31313
10
- qwt/null_paintdevice.py,sha256=1pgbcvyRTVN32RGzuPzUOm0iUImjvPQ4l5OfJVyA3hg,9375
10
+ qwt/null_paintdevice.py,sha256=MQ_H4yc9qMqjAoCziaP3Sa6Mnu9wMSpbxTDisI7j1Dw,9381
11
11
  qwt/painter.py,sha256=k345puF4MAoTye0WBveiTNpCNWXx1K2o62_I0PZYlIA,16536
12
12
  qwt/painter_command.py,sha256=7D3goNsX_2Ufr5kCpC3Cr1t8GoD5Yz0G1oIZJKvoGBY,8501
13
- qwt/plot.py,sha256=d8rf4F-fpQgdpTTqNKQFJh7B0FlQ4WlquooeJQRn_eo,75660
14
- qwt/plot_canvas.py,sha256=INRcxB17dvyGI7Jqi2FX4gm16EG4gzJKULj9inHkibA,30309
13
+ qwt/plot.py,sha256=qw35nDxdDcrH-5MxijNo73r3xZ7EYb0S1UiHpavhEVE,77707
14
+ qwt/plot_canvas.py,sha256=KK4saSTuoz5rab2xaQCAimlQlNQIo6GE3TVmHhMdsIc,31038
15
15
  qwt/plot_curve.py,sha256=ThlfxUjB4yiP_Dh96P7PXusyqUfDWcRcyt31OHvu2vk,37059
16
16
  qwt/plot_directpainter.py,sha256=JCWNhCC21L1A1HZnxd-C95QNlrVJbLztmGab-X8gCGc,10165
17
17
  qwt/plot_grid.py,sha256=h-cDTzibEzQyX2g25SBg5ysyXwmONmMFy9NcuzFySYc,16607
@@ -19,14 +19,15 @@ qwt/plot_layout.py,sha256=tD94nsFzWt1LQNFxgZbYsyM66ib6I6wBK3ObS7M1w_o,46183
19
19
  qwt/plot_marker.py,sha256=6vDHdMsyGClMruWLA4lChUCpoLzmxGNkvmICm0KjP6I,21284
20
20
  qwt/plot_renderer.py,sha256=v-fSwicMGQBa_kQx4hJJptlVNpNsKbgWk9jjqG-r_1g,27201
21
21
  qwt/plot_series.py,sha256=Wa_Wunu-t_L8vl95RJq6__k-XA8UV2YNIsc8yNGpTT8,10654
22
+ qwt/qtdesigner.py,sha256=9oXSPD3m6vk1Tc8KLUM7daRuELXnOvNumbo1hlcrvJ4,6753
22
23
  qwt/qthelpers.py,sha256=GpvLr96Ip8SIGwyFfs0f7ocHyKPB7PEjHBQyrpw84xw,1405
23
24
  qwt/scale_div.py,sha256=_iI5cy4s8H6wigE8xsn7W44T2-DqVADqdjQ31uJPa_I,9266
24
- qwt/scale_draw.py,sha256=KYal9YOpvHjFWGkBFPVVNq_64cxM1vsHg_f_n-2nKNU,40547
25
+ qwt/scale_draw.py,sha256=ZE8NSm3qSqwGtYo236mUZT5OC7weTwTWo_GZorhAIN4,40545
25
26
  qwt/scale_engine.py,sha256=7KQB5k-JXuJMsEMQXBJBNppCN-rTN97UKQCYXdazvqQ,36674
26
27
  qwt/scale_map.py,sha256=rHmPv7e9voEFuAIInQJq60mTtIGrLMViNlgJYu20znA,9605
27
28
  qwt/scale_widget.py,sha256=p202DrRgMraGbVcubzkatvVXOLbO9CPho729hz6gqa0,27125
28
29
  qwt/symbol.py,sha256=DIXtIOgniO70pScKF-H_mee8GgBuycW3YcsopvfCRGY,39147
29
- qwt/text.py,sha256=DrrWbEzYA65kcU8aBcKu5YpQ2LzPnOmAO3R7jF8s0J8,48058
30
+ qwt/text.py,sha256=GeA18Hn0WBDwKZRwk3jAXMkOmiS-FAL9jfLri8qF0tY,47674
30
31
  qwt/toqimage.py,sha256=H2u_qDt32n7HGZakWvs3Ve0G3u04Uk2ZQYp8MuZkkmU,1723
31
32
  qwt/transform.py,sha256=bFkdmI2wDjOJrUZU6E9-sAvwoo_umXycxq6LPvD6N_c,6078
32
33
  qwt/tests/__init__.py,sha256=diBkA_H_Xuxb84tNelsNU4djTlUnKJ5HZNZr02EwnjE,1102
@@ -45,16 +46,22 @@ qwt/tests/test_errorbar.py,sha256=ecJ_YHcx1RhCS9_BKKoNYz6bOuuqapRPGL2Vp_r3O1U,10
45
46
  qwt/tests/test_eventfilter.py,sha256=31vCsCzRcoq3cyMlDAH5fthA45QqeFXKaWyhfZME6rE,16441
46
47
  qwt/tests/test_highdpi.py,sha256=Xafo016q-RX4918JddBGD60HZ4GpuYJBJokEO15md24,1053
47
48
  qwt/tests/test_image.py,sha256=yY_AplKEdm-M7uB5G50p4E68ZzNamYvGSfN5rvlVPPM,6549
49
+ qwt/tests/test_issue107_gdi_leak.py,sha256=7IgZd9vJyD0_ea4CIDJ_SjlYDwYkA94aSRWEqt4y2ew,2203
50
+ qwt/tests/test_issue109_backingstore.py,sha256=2PbNmobG7QsdXhE-vgPiKI6TaXbAz9m6G_qIDIkvMa8,2461
51
+ qwt/tests/test_issue33_sidebyside.py,sha256=Tuu6XcRrV4ucPXmR8Y94uegRwCDWAN40EfuDQyVKHzE,10186
52
+ qwt/tests/test_issue88_mousetracking.py,sha256=jr6iSBb1LIa32Rqt8opBXElC_ar0ru1j8XG1RUM_y5c,2360
48
53
  qwt/tests/test_loadtest.py,sha256=ZTHatcGAbLyvS0BpbOWbzPD2Bjz4rckF7oxZsUVQ5KM,1812
49
54
  qwt/tests/test_logcurve.py,sha256=XOtR0asSqFfzc0VL0qfXN0fXNvxvLnz5fxmNZ2gzQWk,1129
50
55
  qwt/tests/test_mapdemo.py,sha256=e7rvB8IPKYVVii8UgdHSbOHLc3i7eB21EQNKZ151Kc8,3423
51
56
  qwt/tests/test_multidemo.py,sha256=YS-P08qcakD41OJy6ZI53ofKOnUvD-2_uum0yOLiIRc,2530
57
+ qwt/tests/test_qtdesigner.py,sha256=DOVapZrBvNAIA1GS0zFyLkA4Wxlyllh5lMhuGVfL2gc,1542
58
+ qwt/tests/test_qtdesigner.ui,sha256=iqxZYkst-ZhBe08YVeptVvbohathyEHFxsHtErpHMj0,685
52
59
  qwt/tests/test_relativemargin.py,sha256=Shn0aE7UeETSSaQFpt4DrMOFeMN3p4rmEHKqPhXfQTI,2097
53
60
  qwt/tests/test_simple.py,sha256=4opQp_-RkcVdpRyYSh89ddBkeXCPrppB-_2KTiu5vjI,2347
54
61
  qwt/tests/test_stylesheet.py,sha256=cuL1iY4pp5ZoMX4OI86amJA6R5SzQsH1JUWLtEiitZ4,950
55
62
  qwt/tests/test_symbols.py,sha256=YcGM0_Em8QtcbqpLSU98PxvGo7BIIYviP2o_4Fg2LbI,5607
56
63
  qwt/tests/test_vertical.py,sha256=hWhPASIRb1nrd6d3m3dPoccwl5RoCVH_r_C8VK2znMI,3285
57
- qwt/tests/utils.py,sha256=kmAl8B0yKq1FCzVU4pBDAXwcQUGa4BE8m6FsNECEVEQ,10584
64
+ qwt/tests/utils.py,sha256=5DdF53mL9VEn-f2WjXL4LMRO2MaUMFUvsKFprHnN3nw,10646
58
65
  qwt/tests/data/PythonQwt.svg,sha256=t-NYdVPqz7BcqtXNWCJvo8udrRmtGh57ksSYWUl1h4o,15232
59
66
  qwt/tests/data/bodedemo.png,sha256=8jP1YLE6HdwjEmrQyJclLxiYI4Zs4t0z0BoN1JT3GPQ,24414
60
67
  qwt/tests/data/cartesian.png,sha256=Cwbn5Jy3VT-OisFHxzMCVbfdbJacUMZ9uGCD9sCxA-Q,51094
@@ -77,8 +84,8 @@ qwt/tests/data/symbol.svg,sha256=ONkkohVqpg3OO2-XEZge7UZLqqNyOQXbSpLgHXcTBFU,127
77
84
  qwt/tests/data/symbols.png,sha256=wiqRBc2MJluJ-am9LtZ0K9DHV9BnreYMiEvfzJUFzCk,55586
78
85
  qwt/tests/data/testlauncher.png,sha256=WmucObp30QYYKsVRBs5-6t1aEN0FbIWRmOVFN1gt-vQ,139676
79
86
  qwt/tests/data/vertical.png,sha256=EvyG6q6rZm6pTT08Y_nzdBYCnqmoX1QDuEQtp2pyGlw,34076
80
- pythonqwt-0.16.1.dist-info/METADATA,sha256=bE1-gjZ7myzMI-HDjjIwU-zE6mBgkDAS74jYVfPUwqA,45515
81
- pythonqwt-0.16.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
82
- pythonqwt-0.16.1.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
83
- pythonqwt-0.16.1.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
84
- pythonqwt-0.16.1.dist-info/RECORD,,
87
+ pythonqwt-0.16.3.dist-info/METADATA,sha256=X3YbzcMwmBSzowUYsNzto4vrDG8cbHUiqNFRDK5GzDM,45515
88
+ pythonqwt-0.16.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
89
+ pythonqwt-0.16.3.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
90
+ pythonqwt-0.16.3.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
91
+ pythonqwt-0.16.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
qwt/__init__.py CHANGED
@@ -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.1"
66
+ __version__ = "0.16.3"
67
67
  QWT_VERSION_STR = "6.1.5"
68
68
 
69
69
 
qwt/null_paintdevice.py CHANGED
@@ -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 = 1
278
+ value = 0x10000
279
279
  else:
280
280
  value = super(QwtNullPaintDevice, self).metric(deviceMetric)
281
281
  return value
qwt/plot.py CHANGED
@@ -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
- if obj is self.__data.canvas:
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
- hint = self.__data.layout.minimumSizeHint(self)
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.__data.plot:
1850
+ if plot is self.plot():
1819
1851
  return
1820
1852
 
1821
- if self.__data.plot:
1822
- self.__data.plot.attachItem(self, False)
1853
+ current = self.plot()
1854
+ if current is not None:
1855
+ current.attachItem(self, False)
1823
1856
 
1824
- self.__data.plot = plot
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 self.__data.plot:
1827
- self.__data.plot.attachItem(self, True)
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
- return self.__data.plot
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
- if self.__data.plot:
1885
- self.__data.plot.attachItem(self, False)
1924
+ plot = self.plot()
1925
+ if plot is not None:
1926
+ plot.attachItem(self, False)
1886
1927
  self.__data.z = z
1887
- if self.__data.plot:
1888
- self.__data.plot.attachItem(self, True)
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
- if self.__data.plot:
2095
- self.__data.plot.autoRefresh()
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
- if self.testItemAttribute(QwtPlotItem.Legend) and self.__data.plot:
2106
- self.__data.plot.updateLegend(self)
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
  """
qwt/plot_canvas.py CHANGED
@@ -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
- self.__plot = plot
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
- return self.__plot
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().drawCanvas(painter)
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
qwt/qtdesigner.py ADDED
@@ -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
qwt/scale_draw.py CHANGED
@@ -607,7 +607,7 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
607
607
  * start: Start border distance
608
608
  * end: End border distance
609
609
  """
610
- start, end = 0, 1.0
610
+ start, end = 0, 1
611
611
 
612
612
  if not self.hasComponent(QwtAbstractScaleDraw.Labels):
613
613
  return start, end
@@ -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()