Feature Request: Expose QPainter methods that accept pointers

Ognyan Moore ognyan.moore at gmail.com
Thu May 26 19:46:04 BST 2022


Hi Phil,

Thanks for providing this.  One of our regular contributors just started
testing, one issue came up as a compiler failure in PyQt6_sip:sip_array.c
line 261 on MSVC, which was fixed by casting to (char*) before doing the
pointer arithmetic.

The implementation we're using is

segs = Qt.sip.array(QTCore.QLineF, npts - 1)
vp = Qt.sip.voidptr(segs, len(segs) * 4 * 8
memory = np.frombuffer(vp, dtype=np.float64).reshape((-1, 4))
# numpy operations
memory[:, 0] = x[:-1]
memory[:, 1] = y[:-1]
memory[:, 2] = x[1:]
memory[:, 3] = y[1:]

# draw
painter.drawLines(segs)

Thanks again for this, we need to profile/test more.

On Thu, May 26, 2022 at 2:16 AM Phil Thompson <phil at riverbankcomputing.com>
wrote:

> The current snapshots (PyQt5, PyQt5-sip, PyQt6, PyQt6-sip) allow you to
> create a sip.array object like this...
>
> from PyQt6.sip import array
> from PyQt6.QtCore import QPoint
>
> points = array(QPoint, 4)
>
> The array is mutable, so...
>
> points[1].setX(10)
> points[1].setY(20)
>
> The SIP support for the /Array/ annotation has been changed so that any
> argument so marked will accept either a sip.array of the appropriate
> type (in which case no conversions are necessary) or a Python sequence
> (in which case the sequence is converted to a temporary array on the
> fly).
>
> All of the relevant QPainter methods now have support for /Array/, so...
>
> painter.drawPoints(points)
>
> You can test if the support is present just by checking if the sip
> module has the 'array' attribute.
>
> Please test.
>
> Phil
>
>
> On 11/05/2022 16:23, Ognyan Moore wrote:
> > 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/20220526/fb9e0556/attachment-0001.htm>


More information about the PyQt mailing list