Home > front end >  Mouse Event Detection inconsistent with customized QGraphicsItem in pyqtgraph plot
Mouse Event Detection inconsistent with customized QGraphicsItem in pyqtgraph plot

Time:12-16

I modified the paint medthod of a pixmap item so it always draws scaled as a percent of the widget height and centered around the x coordinate of the position it has been placed at.

However, the resultant items do not properly detect when they have been clicked.

In my example much of the area below roi1 reports "got me" and I can't find anywhere that reports got me on roi2.

import pyqtgraph as pg
from PyQt5 import QtWidgets, QtGui, QtCore
import numpy as np
from PyQt5.QtCore import Qt
import logging


class ScaleInvariantIconItem(QtWidgets.QGraphicsPixmapItem):
    def __init__(self,*args, **kwargs):
        self.id = kwargs.pop("id", "dummy")
        self.count =0
        super().__init__(*args, **kwargs)
        self.setPixmap(QtWidgets.QLabel().style().standardPixmap(QtWidgets.QStyle.SP_FileDialogStart))
        self.scale_percent = .25
        self._pen = None

    def setPen(self, pen):
        self._pen=pen
        self.update()

    def mousePressEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
        print("got me", self.id, self.count)
        self.count  = 1

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionGraphicsItem', widget: QtWidgets.QWidget):

        h_scene = self.scene().parent().height()
        h = self.pixmap().height()


        t = painter.transform();
        s = (self.scale_percent*h_scene)/h
        self.setTransformOriginPoint(self.pixmap().width()/2,0)
        painter.save()

        painter.setTransform(QtGui.QTransform(s, t.m12(), t.m13(),
                                              t.m21(), s, t.m23(),
                                              t.m31(), t.m32(), t.m33()))
        painter.translate(-self.pixmap().width() / 2, 0)

        super().paint(painter, option, widget)
        if self._pen:
            painter.setPen(self._pen)
        painter.drawRect(self.pixmap().rect())
        painter.restore()



app = QtWidgets.QApplication([])
pg.setConfigOption('leftButtonPan', False)

g = pg.PlotWidget()
#g = pg.PlotWidget()

QtWidgets.QGraphicsRectItem


roi = ScaleInvariantIconItem(id=1)

roi2 = ScaleInvariantIconItem(id=2)
roi2.setPos(10,20)
roi2.setPen(pg.mkPen('g'))

vb = g.plotItem.getViewBox()

vb.setXRange(-20,20)
vb.setYRange(-20,20)
g.addItem(roi)
#g.addItem(roi2)
g.addItem(roi2)
g.show()
app.exec_()

CodePudding user response:

Changing the way an item is painted does not change its geometry (the "bounding rectangle").

In fact, you're "lucky" that, due to the way pyqtgraph behaves, you're not getting drawing artifacts, as you're actually drawing outside the bounding rect of the pixmap item. As per documentation of very bad painting implementation

As you can see from the picture above, the actual rectangle of the item is very different from the one you're painting, and if you click on the new rectangles you'll properly get the relative mouse events.

Now, the problem is that pyqtgraph uses a complex system of QGraphicsItems to show its contents, and addItem actually adds items to its internal plotItem, using its transformation and relative coordinate system.

If you do not need direct relation and interaction with other items, and you're fine with fixed positions, a possibility is to subclass PlotWidget (which is a QGraphicsView subclass itself), and do the following:

  • overwrite and override addItem (which is overwritten by PlotWidget and wraps to the underlying PlotItem object methods), so that you can add "scalable" items to the scene, instead of adding them the PlotItem; doing this, you also need to create a reference to the plot item for the scalable item;
  • add a function to your item that scales itself based on the actual view size (not the view box!), and positions itself based on the viewbox range;
  • override setPos of your item to keep a reference to the position based on the viewbox, instead of that of the scene;
  • install an event filter on the PlotItem to get resize events and eventually rescale/reposition the items;
  • connect to the sigRangeChanged signal of the PlotItem to a timer that actually calls the function above (this has to be delayed due to the event queueing, as instant calls would result in unreliable results);

This is a possible implementation of the above:

class ScaleInvariantIconItem(QtWidgets.QGraphicsPixmapItem):
    _pos = None
    _pen = None
    def __init__(self,*args, **kwargs):
        self.id = kwargs.pop("id", "dummy")
        self.count = 0
        super().__init__(*args, **kwargs)
        self.basePixmap = QtWidgets.QApplication.style().standardPixmap(
            QtWidgets.QStyle.SP_FileDialogStart)
        self.setPixmap(self.basePixmap)
        self.scale_percent = .25

    def setPos(self, *args):
        if len(args) == 1:
            self._pos = args[0]
        else:
            self._pos = QtCore.QPointF(*args)

    def relativeResize(self, size):
        newPixmap = self.basePixmap.scaled(
            size * self.scale_percent, QtCore.Qt.KeepAspectRatio)
        self.setPixmap(newPixmap)
        pos = self.plotItem.getViewBox().mapViewToScene(self._pos or QtCore.QPointF())
        super().setPos(pos - QtCore.QPointF(newPixmap.width() / 2, 0))

    def setPen(self, pen):
        self._pen = pen
        self.update()

    def mousePressEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
        print("got me", self.id, self.count)
        self.count  = 1

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionGraphicsItem', widget: QtWidgets.QWidget):
        super().paint(painter, option, widget)
        if self._pen:
            painter.setPen(self._pen)
        painter.drawRect(self.pixmap().rect())


class PlotWidget(pg.PlotWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scalableItems = []
        self.plotItemAddItem, self.addItem = self.addItem, self._addItem
        self.plotItem.installEventFilter(self)
        self.delayTimer = QtCore.QTimer(
            interval=0, timeout=self.updateScalableItems, singleShot=True)
        self.plotItem.sigRangeChanged.connect(self.delayTimer.start)

    def updateScalableItems(self):
        size = self.size()
        for item in self.scalableItems:
            item.relativeResize(size)

    def eventFilter(self, obj, event):
        if event.type() == QtWidgets.QGraphicsSceneResizeEvent:
            self.updateScalableItems()
        return super().eventFilter(obj, event)

    def _addItem(self, item):
        if isinstance(item, ScaleInvariantIconItem):
            item.plotItem = self.plotItem
            self.scalableItems.append(item)
            self.scene().addItem(item)
        else:
            self.plotItemAddItem(item)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        if event:
            # pyqtgraph calls resizeEvent with a None arguments during
            # initialization, we should ignore it
            self.updateScalableItems()

# ...
# use the custom subclass
g = PlotWidget()
# ...

Note that:

  • this will only work as long as you only have a single view; while this is usually not an issue for pyqtgraph, a QGraphicsScene can be actually be shown at the same time in multiple QGraphicsViews, just like item models in item views;
  • to get the default style, don't create a new QWidget instance, just access the QApplication style();
  • spaces are very important for code readability (which is often even more important than other things, like typing); read more on the official Style Guide for Python Code (aka, PEP-8);
  • Related