Skip to content

Commit

Permalink
Added support for QtPy 2, PyQt6, PySide6
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreRaybaut committed Dec 27, 2021
1 parent 4fc7a03 commit 58a5d38
Show file tree
Hide file tree
Showing 25 changed files with 149 additions and 188 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# PythonQwt Releases

## Version 0.10.0

- Added support for QtPy 2, PyQt6 and PySide6.
- Dropped support for Python 2.

## Version 0.9.2

- Curve plotting: added support for `numpy.float32` data type.
Expand Down
64 changes: 5 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ from qwt import tests
tests.run()
```

or from the command line (script name depends on Python major version number):
or from the command line:

```bash
PythonQwt-py3
PythonQwt
```

Tests may also be executed in unattended mode:

```bash
PythonQwt-tests-py3 --mode unattended
PythonQwt-tests --mode unattended
```

## Overview
Expand All @@ -87,65 +87,11 @@ for more details on API limitations when comparing to Qwt.

### Requirements

- Python >=2.6 or Python >=3.2
- PyQt4 >=4.4 or PyQt5 >= 5.5 (or PySide2, still experimental, see below)
- Python >=3.4
- PyQt4, PyQt5, PyQt6 or PySide6
- QtPy >= 1.3
- NumPy >= 1.5

### Why PySide2 support is still experimental

![PyQt5 vs PySide2](doc/images/pyqt5_vs_pyside2.png)

Try running the `curvebenchmark1.py` test with PyQt5 and PySide: you will notice a
huge performance issue with PySide2 (see screenshot above). This is due to the fact
that `QPainter.drawPolyline` (the `QPainter.drawPolyline` method has already been
optimized thanks to Cristian Maureira-Fredes from Python-Qt development team, see
[this bug report](https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1366)) is
much more efficient in PyQt5 than it is in PySide2 (see
[this bug report](https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1540)).

As a consequence, until this bug is fixed in PySide2, we still recommend using PyQt5
instead of PySide2 when it comes to representing huge data sets (except if you do not
use the "dots" style for drawing curves).

However, PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
V0.8.1 thanks to the new `array2d_to_qpolygonf` function (see the part related to
PySide2 in the code below).

```python
def array2d_to_qpolygonf(xdata, ydata):
"""
Utility function to convert two 1D-NumPy arrays representing curve data
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy).
License/copyright: MIT License © Pierre Raybaut 2020.
:param numpy.ndarray xdata: 1D-NumPy array (numpy.float64)
:param numpy.ndarray ydata: 1D-NumPy array (numpy.float64)
:return: Polyline
:rtype: QtGui.QPolygonF
"""
dtype = np.float64
if not (
xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]
and xdata.dtype == ydata.dtype == dtype
):
raise ValueError("Arguments must be 1D, float64 NumPy arrays with same size")
size = xdata.size
polyline = QPolygonF(size)
if PYSIDE2: # PySide2 (obviously...)
address = shiboken2.getCppPointer(polyline.data())[0]
buffer = (ctypes.c_double * 2 * size).from_address(address)
else: # PyQt4, PyQt5
buffer = polyline.data()
buffer.setsize(2 * size * np.finfo(dtype).dtype.itemsize)
memory = np.frombuffer(buffer, dtype)
memory[: (size - 1) * 2 + 1 : 2] = xdata
memory[1 : (size - 1) * 2 + 2 : 2] = ydata
return polyline
```

## Installation

From the source package:
Expand Down
4 changes: 2 additions & 2 deletions doc/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ The two lines above execute the ``PythonQwt`` test launcher:
.. image:: /../qwt/tests/data/testlauncher.png

GUI-based test launcher can be executed from the command line thanks to the
``PythonQwt-py3`` test script (or ``PythonQwt-py2`` for Python 2).
``PythonQwt`` test script.

Unit tests may be executed from the commande line thanks to the console-based script
``PythonQwt-tests-py3``: ``PythonQwt-tests-py3 --mode unattended``.
``PythonQwt-tests``: ``PythonQwt-tests --mode unattended``.

Tests
-----
Expand Down
Binary file modified doc/images/QwtPlot_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed doc/images/pyqt5_vs_pyside2.png
Binary file not shown.
Binary file modified doc/images/symbol_path_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 2 additions & 21 deletions doc/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Dependencies
------------

Requirements:
* Python 2.x (x>=6) or 3.x (x>=2)
* PyQt4 4.x (x>=4) or PyQt5 5.x (x>=5) or PySide2 (still experimental, see below)
* Python 3.x (x>=4)
* PyQt4 4.x (x>=4), PyQt5 5.x (x>=5), PyQt6 or PySide6
* QtPy >= 1.3
* NumPy 1.x (x>=5)
* Sphinx 1.x (x>=1) for documentation generation
Expand All @@ -18,25 +18,6 @@ From the source package:

`python setup.py install`

Why PySide2 support is still experimental
-----------------------------------------

.. image:: /images/pyqt5_vs_pyside2.png

Try running the `curvebenchmark1.py` test with PyQt5 and PySide: you will notice a
huge performance issue with PySide2 (see screenshot above). This is due to the fact
that `QPainter.drawPolyline` is much more efficient in PyQt5 than it is in PySide2
(see `this bug report <https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1366>`_).

As a consequence, until this bug is fixed in PySide2, we still recommend using PyQt5
instead of PySide2 when it comes to representing huge data sets.

However, PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
V0.8.1 thanks to the new `array2d_to_qpolygonf` function (see code below).

.. literalinclude:: /../qwt/plot_curve.py
:pyobject: array2d_to_qpolygonf

Help and support
----------------

Expand Down
6 changes: 2 additions & 4 deletions qwt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical
user interfaces for the Python programming language. It is compatible with
both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it
could be in the near future as it would "only" requires testing to support
it as a stable alternative to PyQt).
``PyQt4``, ``PyQt5``, ``PyQt6`` and ``PySide6``.
It consists of a single Python package named `qwt` which is a pure Python
implementation of Qwt C++ library with some limitations.
Expand All @@ -28,7 +26,7 @@
.. _GitHubPage: http://pierreraybaut.github.io/PythonQwt
.. _GitHub: https://github.com/PierreRaybaut/PythonQwt
"""
__version__ = "0.9.2"
__version__ = "0.10.0"
QWT_VERSION_STR = "6.1.5"

import warnings
Expand Down
3 changes: 1 addition & 2 deletions qwt/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,6 @@ def sizeHint(self):
sz.setHeight(max([sz.height(), self.__data.icon.height() + 4]))
if self.__data.itemMode != QwtLegendData.ReadOnly:
sz += buttonShift(self)
sz = sz.expandedTo(QApplication.globalStrut())
return sz

def paintEvent(self, e):
Expand Down Expand Up @@ -912,7 +911,7 @@ def renderLegend(self, painter, rect, fillBackground):
legendLayout = self.__data.view.contentsWidget.layout()
if legendLayout is None:
return
left, right, top, bottom = self.getContentsMargins()
left, right, top, bottom = self.layout().getContentsMargins()
layoutRect = QRect()
layoutRect.setLeft(math.ceil(rect.left()) + left)
layoutRect.setTop(math.ceil(rect.top()) + top)
Expand Down
7 changes: 5 additions & 2 deletions qwt/null_paintdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
:members:
"""

import os

from qtpy.QtGui import QPaintEngine, QPainterPath, QPaintDevice
from qtpy import PYSIDE2

QT_API = os.environ["QT_API"]


class QwtNullPaintDevice_PrivateData(object):
Expand Down Expand Up @@ -59,7 +62,7 @@ def drawLines(self, lines, lineCount=None):
device = self.nullDevice()
if device is None:
return
if device.mode() != QwtNullPaintDevice.NormalMode and not PYSIDE2:
if device.mode() != QwtNullPaintDevice.NormalMode and QT_API.startswith("pyqt"):
try:
QPaintEngine.drawLines(lines, lineCount)
except TypeError:
Expand Down
4 changes: 0 additions & 4 deletions qwt/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,10 +1437,6 @@ def drawItems(self, painter, canvasRect, maps):
QPainter.Antialiasing,
item.testRenderHint(QwtPlotItem.RenderAntialiased),
)
painter.setRenderHint(
QPainter.HighQualityAntialiasing,
item.testRenderHint(QwtPlotItem.RenderAntialiased),
)
item.draw(painter, maps[item.xAxis()], maps[item.yAxis()], canvasRect)
painter.restore()

Expand Down
31 changes: 20 additions & 11 deletions qwt/plot_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
:members:
"""

import os

from qwt.null_paintdevice import QwtNullPaintDevice
from qwt.painter import QwtPainter

from qtpy import PYQT5
from qtpy.QtGui import (
QPaintEngine,
QPen,
Expand All @@ -30,11 +31,20 @@
qAlpha,
QPolygonF,
)
from qtpy.QtWidgets import QFrame, QStyleOption, QStyle, QStyleOptionFrame
from qtpy.QtWidgets import QFrame, QStyleOption, QStyle
from qtpy.QtCore import Qt, QSizeF, QEvent, QPointF, QRectF
from qtpy import QtCore as QC


QT_MAJOR_VERSION = int(QC.__version__.split(".")[0])
QT_API = os.environ["QT_API"]

if QT_API in ("pyqt", "pyqt4"):
from PyQt4.QtGui import QStyleOptionFrameV3 as QStyleOptionFrame
elif QT_API == "pyside2":
from PySide2.QtWidgets import QStyleOptionFrame
else:
from qtpy.QtWidgets import QStyleOptionFrame


class Border(object):
Expand Down Expand Up @@ -126,7 +136,7 @@ def alignCornerRects(self, rect):
def _rects_conv_PyQt5(rects):
# PyQt5 compatibility: the conversion from QRect to QRectF should not
# be necessary but it seems to be anyway... PyQt5 bug?
if PYQT5:
if QT_API == "pyqt5":
return [QRectF(rect) for rect in rects]
else:
return rects
Expand Down Expand Up @@ -172,7 +182,13 @@ def qwtDrawBackground(painter, canvas):
else:
painter.setPen(Qt.NoPen)
painter.setBrush(brush)
painter.drawRects(_rects_conv_PyQt5(painter.clipRegion().rects()))
clipregion = painter.clipRegion()
try:
rects = clipregion.rects()
except AttributeError:
# Qt6: no equivalent to 'rects' method...
rects = [clipregion.begin()]
painter.drawRects(_rects_conv_PyQt5(rects))

painter.restore()

Expand Down Expand Up @@ -731,13 +747,6 @@ def drawBorder(self, painter):
self.frameStyle(),
)
else:
if PYQT5:
from qtpy.QtWidgets import QStyleOptionFrame
else:
try:
from PyQt4.QtGui import QStyleOptionFrameV3 as QStyleOptionFrame
except ImportError:
from PySide2.QtWidgets import QStyleOptionFrame
opt = QStyleOptionFrame()
opt.initFrom(self)
frameShape = self.frameStyle() & QFrame.Shape_Mask
Expand Down
46 changes: 31 additions & 15 deletions qwt/plot_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
:members:
"""

import os

from qwt.text import QwtText
from qwt.plot import QwtPlot, QwtPlotItem, QwtPlotItem_PrivateData
from qwt._math import qwtSqr
Expand All @@ -27,12 +29,16 @@
from qwt.plot_directpainter import QwtPlotDirectPainter
from qwt.qthelpers import qcolor_from_str

from qtpy import PYSIDE2
from qtpy.QtGui import QPen, QBrush, QPainter, QPolygonF, QColor
from qtpy.QtCore import QSize, Qt, QRectF, QPointF

if PYSIDE2:
import shiboken2
QT_API = os.environ["QT_API"]

if QT_API == "pyside2":
import shiboken2 as shiboken
import ctypes
elif QT_API == "pyside6":
import shiboken6 as shiboken
import ctypes

import numpy as np
Expand Down Expand Up @@ -62,12 +68,12 @@ def qwtVerifyRange(size, i1, i2):

def array2d_to_qpolygonf(xdata, ydata):
"""
Utility function to convert two 1D-NumPy arrays representing curve data
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
Utility function to convert two 1D-NumPy arrays representing curve data
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy).
License/copyright: MIT License © Pierre Raybaut 2020-2021.
:param numpy.ndarray xdata: 1D-NumPy array
:param numpy.ndarray ydata: 1D-NumPy array
:return: Polyline
Expand All @@ -76,11 +82,16 @@ def array2d_to_qpolygonf(xdata, ydata):
if not (xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]):
raise ValueError("Arguments must be 1D NumPy arrays with same size")
size = xdata.size
polyline = QPolygonF(size)
if PYSIDE2: # PySide2 (obviously...)
address = shiboken2.getCppPointer(polyline.data())[0]
if QT_API.startswith("pyside"): # PySide (obviously...)
if QT_API == "pyside2":
polyline = QPolygonF(size)
else:
polyline = QPolygonF()
polyline.resize(size)
address = shiboken.getCppPointer(polyline.data())[0]
buffer = (ctypes.c_double * 2 * size).from_address(address)
else: # PyQt4, PyQt5
polyline = QPolygonF(size)
buffer = polyline.data()
buffer.setsize(16 * size) # 16 bytes per point: 8 bytes per X,Y value (float64)
memory = np.frombuffer(buffer, np.float64)
Expand Down Expand Up @@ -685,7 +696,12 @@ def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to):
:py:meth:`draw()`, :py:meth:`drawSticks()`,
:py:meth:`drawDots()`, :py:meth:`drawLines()`
"""
polygon = QPolygonF(2 * (to - from_) + 1)
size = 2 * (to - from_) + 1
if QT_API == "pyside6":
polygon = QPolygonF()
polygon.resize(size)
else:
polygon = QPolygonF(size)
inverted = self.orientation() == Qt.Vertical
if self.__data.attributes & self.Inverted:
inverted = not inverted
Expand Down Expand Up @@ -787,14 +803,14 @@ def closePolyline(self, painter, xMap, yMap, polygon):
if yMap.transformation():
baseline = yMap.transformation().bounded(baseline)
refY = yMap.transform(baseline)
polygon += QPointF(polygon.last().x(), refY)
polygon += QPointF(polygon.first().x(), refY)
polygon.append(QPointF(polygon.last().x(), refY))
polygon.append(QPointF(polygon.first().x(), refY))
else:
if xMap.transformation():
baseline = xMap.transformation().bounded(baseline)
refX = xMap.transform(baseline)
polygon += QPointF(refX, polygon.last().y())
polygon += QPointF(refX, polygon.first().y())
polygon.append(QPointF(refX, polygon.last().y()))
polygon.append(QPointF(refX, polygon.first().y()))

def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to):
"""
Expand Down
Loading

0 comments on commit 58a5d38

Please sign in to comment.