Feature Request: Expose QPainter methods that accept pointers

Ognyan Moore ognyan.moore at gmail.com
Wed May 11 16:23:04 BST 2022


Hi Phil,

Thanks for giving this your consideration.  There are two common use-cases
our users have.  They have a graph/plot with the underlying data is static,
and they zoom/pan triggering the draw methods for the QPainter (re-drawing
the same lines in the same positions).  The other use-case is a
more-or-less fixed view range, but updating the underlying data.  In this
second use case, I assume that the number of lines that are drawn is fairly
consistent, but their position would be different.  In what I expect is
almost all use cases, the QPainter draw methods are called frequently as it
occurs on every refresh (and a large selling point in our library is the
interactivity).

When discussing your response with other contributors, we still do not
think that knowing the internal layout of QLineF would be an issue as it's
part of Qt's ABI, but if you think depending on that is too risky, you
would know better than we would.

At a glance, either method would likely be beneficial to us over the
current implementation

The first method you describe would seem to amount to accepting a NumPy
array since NumPy supports the buffer protocol.  The second method which
say would have the API of drawLines(PyQtArrayObj) would also be fine
provided the PyQtArrayObj was mutable.

For general info, the number of points/lines we're talking about here is
roughly in the thousands to hundreds of thousands.

On Tue, May 10, 2022 at 9:14 AM Phil Thompson <phil at riverbankcomputing.com>
wrote:

> On 08/05/2022 01:50, Ognyan Moore wrote:
> > Sorry to reply to my own email at this but someone pointed out to me an
> > alternative would be to use voidptr instead of wrapinstance in the
> > following fashion; not sure how that would work given that drawLines is
> > overloaded (might be more feasible for drawPixmapFragments since that's
> > the
> > only signature).
> >
> > lines = np.array([
> >     [0, 0, 0, 10],
> >     [0, 10, 10, 0],
> >     [10, 0, 20, 10]], dtype=np.float64
> > )
> >
> > ptr = sip.voidptr(lines)
> > painter.drawLines(ptr, lines.shape[0])
>
> While the above is the most efficient it depends on knowing the internal
> layout of a QLineF.
>
> There are two more reliable approaches...
>
> The first is to add explicit support for passing a Python buffer object
> of an appropriate size and shape (ie. the 'lines' object above) and
> convert it to a QLineF array on the fly. That has the disadvantage of
> re-creating the array each time the method is called. However if a line
> is only ever drawn once (and the problem you are trying to solve is to
> draw many different lines) then this doesn't matter so much.
>
> The second is to have a more general purpose mechanism for creating an
> array of C++ instances and wrapping it in a bespoke Python object.
> (There is already sip.array which is very similar.) This would mean that
> the conversion of 'lines' to something that the method can use is only
> done once and would be helpful if your problem is when you are drawing
> the same line many times.
>
> Can you be more specific about your use case?
>
> Phil
>
> > On Sat, May 7, 2022 at 1:57 PM Ognyan Moore <ognyan.moore at gmail.com>
> > wrote:
> >
> >> Hi Phil,
> >>
> >> You are correct, PyQt bindings do provide us with the functionality we
> >> need; we're hoping to have the other signatures enabled (assuming it's
> >> not
> >> a high effort task).
> >>
> >> Majority of our data is already in continuous numpy arrays, so we have
> >> a
> >> strong interest in being able to pass those arrays to the draw methods
> >> in a
> >> more direct fashion.  We are hoping to be able to do call the QPainter
> >> draw
> >> methods by putting the array in the correct shape and with the correct
> >> data
> >> type, without having to convert the it to a list, and without having
> >> instantiate or cast each element to QLineF (or QPointF,
> >> QPainter.PixmapFragment, QPolygonF objects).
> >>
> >> With our QLineF instance, we hope to be able to pass the pointer of a
> >> n_lines x 4 numpy array (of type double/float64) and. be able to call
> >> the
> >> drawLines method in something like the following fashion.
> >>
> >> import numpy as np
> >> from PyQt6 import QtCore, QtGui, QtWidgets, sip
> >> import itertools
> >> import sys
> >>
> >> app = QtWidgets.QApplication([])
> >>
> >> # array makeup [[x1, y1, x2, y2]]
> >> lines = np.array([
> >> [0, 0, 0, 10],
> >> [0, 10, 10, 0],
> >> [10, 0, 20, 10]], dtype=np.float64
> >> )
> >>
> >> qimg = QtGui.QImage(20, 20, QtGui.QImage.Format.Format_RGB32)
> >> qimg.fill(0)
> >> painter = QtGui.QPainter(qimg)
> >> painter.setPen(QtCore.Qt.GlobalColor.cyan)
> >>
> >> # desired implementation
> >> https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-4
> >> # ptr = sip.wrapinstance(lines, lines.ctypes.data, QtCore.QLineF)
> >> # painter.drawLines(ptr, lines.shape[0])
> >>
> >> # current implementation
> >> https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-5
> >> ptr = list(map(sip.wrapinstance,
> >> itertools.count(lines.ctypes.data, lines.strides[0]),
> >> itertools.repeat(QtCore.QLineF, lines.shape[0])))
> >> painter.drawLines(ptr)
> >>
> >> painter.end()
> >> qimg.save('drawLines.png')
> >>
> >> For QPainter.drawPixmapFragments (the pyside equivalent of this works
> >> actually, not sure if that was intentional on their part or not)
> >>
> >> import numpy as np
> >> from PyQt6 import QtCore, QtGui, QtWidgets, sip
> >> import itertools
> >>
> >> app = QtWidgets.QApplication([])
> >>
> >> # make the pixmap
> >> pix = QtGui.QPixmap(51, 51)
> >> pix.fill(0)
> >> painter = QtGui.QPainter(pix)
> >> painter.setPen(QtCore.Qt.GlobalColor.cyan)
> >> painter.drawEllipse(0, 0, 50, 50)
> >> painter.end()
> >>
> >> # create numpy array representing fragments
> >> fieldnames = ['x', 'y', 'sourceLeft', 'sourceTop', 'width', 'height',
> >> 'scaleX', 'scaleY', 'rotation', 'opacity']
> >> frags_array = np.zeros(3, dtype=[(name, 'f8') for name in fieldnames])
> >> frags_array['sourceLeft'] = 0
> >> frags_array['sourceTop'] = 0
> >> frags_array['width'] = 51
> >> frags_array['height'] = 51
> >> frags_array['scaleX'] = 1.0
> >> frags_array['scaleY'] = 1.0
> >> frags_array['rotation'] = 0.0
> >> frags_array['opacity'] = 1.0
> >>
> >> frags_array['x'] = [50, 100, 150]
> >> frags_array['y'] = [50, 100, 150]
> >>
> >>
> >> qimg = QtGui.QImage(200, 200, QtGui.QImage.Format.Format_RGB32)
> >> qimg.fill(0)
> >> painter = QtGui.QPainter(qimg)
> >>
> >> # desired implementation
> >> # frag_ptr = sip.wrapinstance(frags_array.ctypes.data,
> >> QtGui.QPainter.PixmapFragment)
> >> # painter.drawPixmapFragments(frag_ptr, frags_array.size, pix)
> >>
> >> # current implementation
> >> frag_ptr = list(map(sip.wrapinstance,
> >> itertools.count(frags_array.ctypes.data, frags_array.strides[0]),
> >> itertools.repeat(QtGui.QPainter.PixmapFragment,
> >> frags_array.shape[0])))
> >> painter.drawPixmapFragments(frag_ptr[:frags_array.shape[0]], pix)
> >> painter.end()
> >> qimg.save('drawPixmapFragments.png')
> >>
> >>
> >> Hopefully that clears things up some.  Thanks!
> >> Ogi
> >>
> >> On Sat, May 7, 2022 at 1:48 AM Phil Thompson
> >> <phil at riverbankcomputing.com>
> >> wrote:
> >>
> >>> On 07/05/2022 03:56, Ognyan Moore wrote:
> >>> > Hi Phil,
> >>> >
> >>> > I am one of the maintainers of pyqtgraph, I'd like to request that
> you
> >>> > enable some of the function signatures for QPainter methods that
> accept
> >>> > pointers.  The following two would be more beneficial for us:
> >>> >
> >>> > QPainter::drawLines(const QLineF *lines, int lineCount)
> >>> > QPainter::drawPixmapFragments(const QPainter::PixmapFragment
> >>> > *fragments,
> >>> > int fragmentCount, const QPixmap &pixmap,
> QPainter::PixmapFragmentHints
> >>> > hints = PixmapFragmentHints())
> >>> >
> >>> > If it would not be too much trouble, enabling some of the other
> >>> > QPainter
> >>> > methods that enabled referencing pointers could also be beneficial,
> but
> >>> > these won't have the same impact as the ones above
> >>> >
> >>> > QPainter::drawConvexPolygon(const QPointF *points, int pointCount)
> >>> > QPainter::drawPoints(const QPointF *points, int pointCount)
> >>> > QPainter::drawPolygon(const QPointF *points, int pointCount,
> >>> > Qt::FillRule
> >>> > fillRule = Qt::OddEvenFill)
> >>> > QPainter::drawRects(const QRectF *rectangles, int rectCount)
> >>> > QPainter::drawPolyline(const QPointF *points, int pointCount)
> >>> >
> >>> > To demonstrate our usage, I'll highlight what we do with the PySide
> >>> > bindings.  For the QPainter::drawPixmapFragments we're able to do
> >>> > something
> >>> > resembling the following
> >>> >
> >>> > import numpy as np
> >>> > size = 1_000
> >>> > arr = np.empty((size, 10), dtype=np.float64)
> >>> > ptrs = shiboken6.wrapInstance(arr.ctypes.data,
> >>> > QtGui.QPainter.PixmapFragment)
> >>> > ...
> >>> > QPainter.drawPixmapFragments(ptrs, size, pixmap)
> >>> >
> >>> > For the equivalent functionality with PyQt bindings
> >>> >
> >>> > ptrs = list(map(sip.wrapInstance,
> >>> >    itertools.count(arr.ctypes.data, arr.strides[0]),
> >>> >    itertools.repeat(QtGui.QPainter.PixmapFragment, arr.shape[0])))
> >>> > QPainter.drawPixmapFragments(ptrs[:size], pixmap)
> >>> >
> >>> >
> >>> > We do this right now with QImage, and in a round-about way with
> >>> > QPolygonF
> >>> > construction.
> >>>
> >>> All of those methods are supported, but maybe not in the way that you
> >>> want.
> >>>
> >>> drawPixmapFragments() takes a list of PixmapFragment. The others
> >>> takes a
> >>> variable number of arguments of the appropriate type, so if you had a
> >>> list of QLineF objects (called lines) you would call
> >>> drawLines(*lines).
> >>>
> >>> Can you clarify?
> >>>
> >>> Phil
> >>>
> >>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20220511/47551a5a/attachment.htm>


More information about the PyQt mailing list