<div dir="ltr">Hi Phil,<div><br></div><div>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.</div><div><br></div><div>The implementation we're using is</div><div><br></div><div>segs = Qt.sip.array(QTCore.QLineF, npts - 1)</div><div>vp = Qt.sip.voidptr(segs, len(segs) * 4 * 8</div><div>memory = np.frombuffer(vp, dtype=np.float64).reshape((-1, 4))</div><div># numpy operations</div><div>memory[:, 0] = x[:-1]</div><div>memory[:, 1] = y[:-1]</div><div>memory[:, 2] = x[1:]</div><div>memory[:, 3] = y[1:]</div><div><br></div><div># draw</div><div>painter.drawLines(segs)</div><div><br></div><div>Thanks again for this, we need to profile/test more.</div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Thu, May 26, 2022 at 2:16 AM Phil Thompson <<a href="mailto:phil@riverbankcomputing.com">phil@riverbankcomputing.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">The current snapshots (PyQt5, PyQt5-sip, PyQt6, PyQt6-sip) allow you to <br>
create a sip.array object like this...<br>
<br>
from PyQt6.sip import array<br>
from PyQt6.QtCore import QPoint<br>
<br>
points = array(QPoint, 4)<br>
<br>
The array is mutable, so...<br>
<br>
points[1].setX(10)<br>
points[1].setY(20)<br>
<br>
The SIP support for the /Array/ annotation has been changed so that any <br>
argument so marked will accept either a sip.array of the appropriate <br>
type (in which case no conversions are necessary) or a Python sequence <br>
(in which case the sequence is converted to a temporary array on the <br>
fly).<br>
<br>
All of the relevant QPainter methods now have support for /Array/, so...<br>
<br>
painter.drawPoints(points)<br>
<br>
You can test if the support is present just by checking if the sip <br>
module has the 'array' attribute.<br>
<br>
Please test.<br>
<br>
Phil<br>
<br>
<br>
On 11/05/2022 16:23, Ognyan Moore wrote:<br>
> Hi Phil,<br>
> <br>
> Thanks for giving this your consideration.  There are two common <br>
> use-cases<br>
> our users have.  They have a graph/plot with the underlying data is <br>
> static,<br>
> and they zoom/pan triggering the draw methods for the QPainter <br>
> (re-drawing<br>
> the same lines in the same positions).  The other use-case is a<br>
> more-or-less fixed view range, but updating the underlying data.  In <br>
> this<br>
> second use case, I assume that the number of lines that are drawn is <br>
> fairly<br>
> consistent, but their position would be different.  In what I expect is<br>
> almost all use cases, the QPainter draw methods are called frequently <br>
> as it<br>
> occurs on every refresh (and a large selling point in our library is <br>
> the<br>
> interactivity).<br>
> <br>
> When discussing your response with other contributors, we still do not<br>
> think that knowing the internal layout of QLineF would be an issue as <br>
> it's<br>
> part of Qt's ABI, but if you think depending on that is too risky, you<br>
> would know better than we would.<br>
> <br>
> At a glance, either method would likely be beneficial to us over the<br>
> current implementation<br>
> <br>
> The first method you describe would seem to amount to accepting a NumPy<br>
> array since NumPy supports the buffer protocol.  The second method <br>
> which<br>
> say would have the API of drawLines(PyQtArrayObj) would also be fine<br>
> provided the PyQtArrayObj was mutable.<br>
> <br>
> For general info, the number of points/lines we're talking about here <br>
> is<br>
> roughly in the thousands to hundreds of thousands.<br>
> <br>
> On Tue, May 10, 2022 at 9:14 AM Phil Thompson <br>
> <<a href="mailto:phil@riverbankcomputing.com" target="_blank">phil@riverbankcomputing.com</a>><br>
> wrote:<br>
> <br>
>> On 08/05/2022 01:50, Ognyan Moore wrote:<br>
>> > Sorry to reply to my own email at this but someone pointed out to me an<br>
>> > alternative would be to use voidptr instead of wrapinstance in the<br>
>> > following fashion; not sure how that would work given that drawLines is<br>
>> > overloaded (might be more feasible for drawPixmapFragments since that's<br>
>> > the<br>
>> > only signature).<br>
>> ><br>
>> > lines = np.array([<br>
>> >     [0, 0, 0, 10],<br>
>> >     [0, 10, 10, 0],<br>
>> >     [10, 0, 20, 10]], dtype=np.float64<br>
>> > )<br>
>> ><br>
>> > ptr = sip.voidptr(lines)<br>
>> > painter.drawLines(ptr, lines.shape[0])<br>
>> <br>
>> While the above is the most efficient it depends on knowing the <br>
>> internal<br>
>> layout of a QLineF.<br>
>> <br>
>> There are two more reliable approaches...<br>
>> <br>
>> The first is to add explicit support for passing a Python buffer <br>
>> object<br>
>> of an appropriate size and shape (ie. the 'lines' object above) and<br>
>> convert it to a QLineF array on the fly. That has the disadvantage of<br>
>> re-creating the array each time the method is called. However if a <br>
>> line<br>
>> is only ever drawn once (and the problem you are trying to solve is to<br>
>> draw many different lines) then this doesn't matter so much.<br>
>> <br>
>> The second is to have a more general purpose mechanism for creating an<br>
>> array of C++ instances and wrapping it in a bespoke Python object.<br>
>> (There is already sip.array which is very similar.) This would mean <br>
>> that<br>
>> the conversion of 'lines' to something that the method can use is only<br>
>> done once and would be helpful if your problem is when you are drawing<br>
>> the same line many times.<br>
>> <br>
>> Can you be more specific about your use case?<br>
>> <br>
>> Phil<br>
>> <br>
>> > On Sat, May 7, 2022 at 1:57 PM Ognyan Moore <<a href="mailto:ognyan.moore@gmail.com" target="_blank">ognyan.moore@gmail.com</a>><br>
>> > wrote:<br>
>> ><br>
>> >> Hi Phil,<br>
>> >><br>
>> >> You are correct, PyQt bindings do provide us with the functionality we<br>
>> >> need; we're hoping to have the other signatures enabled (assuming it's<br>
>> >> not<br>
>> >> a high effort task).<br>
>> >><br>
>> >> Majority of our data is already in continuous numpy arrays, so we have<br>
>> >> a<br>
>> >> strong interest in being able to pass those arrays to the draw methods<br>
>> >> in a<br>
>> >> more direct fashion.  We are hoping to be able to do call the QPainter<br>
>> >> draw<br>
>> >> methods by putting the array in the correct shape and with the correct<br>
>> >> data<br>
>> >> type, without having to convert the it to a list, and without having<br>
>> >> instantiate or cast each element to QLineF (or QPointF,<br>
>> >> QPainter.PixmapFragment, QPolygonF objects).<br>
>> >><br>
>> >> With our QLineF instance, we hope to be able to pass the pointer of a<br>
>> >> n_lines x 4 numpy array (of type double/float64) and. be able to call<br>
>> >> the<br>
>> >> drawLines method in something like the following fashion.<br>
>> >><br>
>> >> import numpy as np<br>
>> >> from PyQt6 import QtCore, QtGui, QtWidgets, sip<br>
>> >> import itertools<br>
>> >> import sys<br>
>> >><br>
>> >> app = QtWidgets.QApplication([])<br>
>> >><br>
>> >> # array makeup [[x1, y1, x2, y2]]<br>
>> >> lines = np.array([<br>
>> >> [0, 0, 0, 10],<br>
>> >> [0, 10, 10, 0],<br>
>> >> [10, 0, 20, 10]], dtype=np.float64<br>
>> >> )<br>
>> >><br>
>> >> qimg = QtGui.QImage(20, 20, QtGui.QImage.Format.Format_RGB32)<br>
>> >> qimg.fill(0)<br>
>> >> painter = QtGui.QPainter(qimg)<br>
>> >> painter.setPen(QtCore.Qt.GlobalColor.cyan)<br>
>> >><br>
>> >> # desired implementation<br>
>> >> <a href="https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-4" rel="noreferrer" target="_blank">https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-4</a><br>
>> >> # ptr = sip.wrapinstance(lines, lines.ctypes.data, QtCore.QLineF)<br>
>> >> # painter.drawLines(ptr, lines.shape[0])<br>
>> >><br>
>> >> # current implementation<br>
>> >> <a href="https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-5" rel="noreferrer" target="_blank">https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-5</a><br>
>> >> ptr = list(map(sip.wrapinstance,<br>
>> >> itertools.count(lines.ctypes.data, lines.strides[0]),<br>
>> >> itertools.repeat(QtCore.QLineF, lines.shape[0])))<br>
>> >> painter.drawLines(ptr)<br>
>> >><br>
>> >> painter.end()<br>
>> >> qimg.save('drawLines.png')<br>
>> >><br>
>> >> For QPainter.drawPixmapFragments (the pyside equivalent of this works<br>
>> >> actually, not sure if that was intentional on their part or not)<br>
>> >><br>
>> >> import numpy as np<br>
>> >> from PyQt6 import QtCore, QtGui, QtWidgets, sip<br>
>> >> import itertools<br>
>> >><br>
>> >> app = QtWidgets.QApplication([])<br>
>> >><br>
>> >> # make the pixmap<br>
>> >> pix = QtGui.QPixmap(51, 51)<br>
>> >> pix.fill(0)<br>
>> >> painter = QtGui.QPainter(pix)<br>
>> >> painter.setPen(QtCore.Qt.GlobalColor.cyan)<br>
>> >> painter.drawEllipse(0, 0, 50, 50)<br>
>> >> painter.end()<br>
>> >><br>
>> >> # create numpy array representing fragments<br>
>> >> fieldnames = ['x', 'y', 'sourceLeft', 'sourceTop', 'width', 'height',<br>
>> >> 'scaleX', 'scaleY', 'rotation', 'opacity']<br>
>> >> frags_array = np.zeros(3, dtype=[(name, 'f8') for name in fieldnames])<br>
>> >> frags_array['sourceLeft'] = 0<br>
>> >> frags_array['sourceTop'] = 0<br>
>> >> frags_array['width'] = 51<br>
>> >> frags_array['height'] = 51<br>
>> >> frags_array['scaleX'] = 1.0<br>
>> >> frags_array['scaleY'] = 1.0<br>
>> >> frags_array['rotation'] = 0.0<br>
>> >> frags_array['opacity'] = 1.0<br>
>> >><br>
>> >> frags_array['x'] = [50, 100, 150]<br>
>> >> frags_array['y'] = [50, 100, 150]<br>
>> >><br>
>> >><br>
>> >> qimg = QtGui.QImage(200, 200, QtGui.QImage.Format.Format_RGB32)<br>
>> >> qimg.fill(0)<br>
>> >> painter = QtGui.QPainter(qimg)<br>
>> >><br>
>> >> # desired implementation<br>
>> >> # frag_ptr = sip.wrapinstance(frags_array.ctypes.data,<br>
>> >> QtGui.QPainter.PixmapFragment)<br>
>> >> # painter.drawPixmapFragments(frag_ptr, frags_array.size, pix)<br>
>> >><br>
>> >> # current implementation<br>
>> >> frag_ptr = list(map(sip.wrapinstance,<br>
>> >> itertools.count(frags_array.ctypes.data, frags_array.strides[0]),<br>
>> >> itertools.repeat(QtGui.QPainter.PixmapFragment,<br>
>> >> frags_array.shape[0])))<br>
>> >> painter.drawPixmapFragments(frag_ptr[:frags_array.shape[0]], pix)<br>
>> >> painter.end()<br>
>> >> qimg.save('drawPixmapFragments.png')<br>
>> >><br>
>> >><br>
>> >> Hopefully that clears things up some.  Thanks!<br>
>> >> Ogi<br>
>> >><br>
>> >> On Sat, May 7, 2022 at 1:48 AM Phil Thompson<br>
>> >> <<a href="mailto:phil@riverbankcomputing.com" target="_blank">phil@riverbankcomputing.com</a>><br>
>> >> wrote:<br>
>> >><br>
>> >>> On 07/05/2022 03:56, Ognyan Moore wrote:<br>
>> >>> > Hi Phil,<br>
>> >>> ><br>
>> >>> > I am one of the maintainers of pyqtgraph, I'd like to request that<br>
>> you<br>
>> >>> > enable some of the function signatures for QPainter methods that<br>
>> accept<br>
>> >>> > pointers.  The following two would be more beneficial for us:<br>
>> >>> ><br>
>> >>> > QPainter::drawLines(const QLineF *lines, int lineCount)<br>
>> >>> > QPainter::drawPixmapFragments(const QPainter::PixmapFragment<br>
>> >>> > *fragments,<br>
>> >>> > int fragmentCount, const QPixmap &pixmap,<br>
>> QPainter::PixmapFragmentHints<br>
>> >>> > hints = PixmapFragmentHints())<br>
>> >>> ><br>
>> >>> > If it would not be too much trouble, enabling some of the other<br>
>> >>> > QPainter<br>
>> >>> > methods that enabled referencing pointers could also be beneficial,<br>
>> but<br>
>> >>> > these won't have the same impact as the ones above<br>
>> >>> ><br>
>> >>> > QPainter::drawConvexPolygon(const QPointF *points, int pointCount)<br>
>> >>> > QPainter::drawPoints(const QPointF *points, int pointCount)<br>
>> >>> > QPainter::drawPolygon(const QPointF *points, int pointCount,<br>
>> >>> > Qt::FillRule<br>
>> >>> > fillRule = Qt::OddEvenFill)<br>
>> >>> > QPainter::drawRects(const QRectF *rectangles, int rectCount)<br>
>> >>> > QPainter::drawPolyline(const QPointF *points, int pointCount)<br>
>> >>> ><br>
>> >>> > To demonstrate our usage, I'll highlight what we do with the PySide<br>
>> >>> > bindings.  For the QPainter::drawPixmapFragments we're able to do<br>
>> >>> > something<br>
>> >>> > resembling the following<br>
>> >>> ><br>
>> >>> > import numpy as np<br>
>> >>> > size = 1_000<br>
>> >>> > arr = np.empty((size, 10), dtype=np.float64)<br>
>> >>> > ptrs = shiboken6.wrapInstance(arr.ctypes.data,<br>
>> >>> > QtGui.QPainter.PixmapFragment)<br>
>> >>> > ...<br>
>> >>> > QPainter.drawPixmapFragments(ptrs, size, pixmap)<br>
>> >>> ><br>
>> >>> > For the equivalent functionality with PyQt bindings<br>
>> >>> ><br>
>> >>> > ptrs = list(map(sip.wrapInstance,<br>
>> >>> >    itertools.count(arr.ctypes.data, arr.strides[0]),<br>
>> >>> >    itertools.repeat(QtGui.QPainter.PixmapFragment, arr.shape[0])))<br>
>> >>> > QPainter.drawPixmapFragments(ptrs[:size], pixmap)<br>
>> >>> ><br>
>> >>> ><br>
>> >>> > We do this right now with QImage, and in a round-about way with<br>
>> >>> > QPolygonF<br>
>> >>> > construction.<br>
>> >>><br>
>> >>> All of those methods are supported, but maybe not in the way that you<br>
>> >>> want.<br>
>> >>><br>
>> >>> drawPixmapFragments() takes a list of PixmapFragment. The others<br>
>> >>> takes a<br>
>> >>> variable number of arguments of the appropriate type, so if you had a<br>
>> >>> list of QLineF objects (called lines) you would call<br>
>> >>> drawLines(*lines).<br>
>> >>><br>
>> >>> Can you clarify?<br>
>> >>><br>
>> >>> Phil<br>
>> >>><br>
>> >><br>
>> <br>
</blockquote></div>