Feature Request: Expose QPainter methods that accept pointers

Phil Thompson phil at riverbankcomputing.com
Thu May 26 10:16:56 BST 2022


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
>> >>>
>> >>
>> 


More information about the PyQt mailing list