Feature Request: Expose QPainter methods that accept pointers

Phil Thompson phil at riverbankcomputing.com
Tue May 10 17:14:28 BST 2022


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