鼠标事件检测与 pyqtgraph 图中的自定义 QGraphicsItem 不一致

Mouse Event Detection inconsistent with customized QGraphicsItem in pyqtgraph plot

我修改了像素图项目的绘制方法,因此它总是按小部件高度的百分比进行绘制,并以其放置位置的 x 坐标为中心。

但是,生成的项目无法正确检测它们何时被单击。

在我的示例中,roi1 下面的大部分区域都报告“找到了我”,但我找不到任何报告让我找到 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_()

更改项目的绘制方式不会更改其几何形状(“边界矩形”)。

事实上,由于 pyqtgraph 的行为方式,您是“幸运的”,您没有得到绘图伪像,因为您实际上是在 边界之外绘制像素图项目的矩形。根据 paint() 的文档:

Make sure to constrain all painting inside the boundaries of boundingRect() to avoid rendering artifacts (as QGraphicsView does not clip the painter for you).

由于 pyqtgraph 将项目添加到其视图框(QGraphicsItem 子类本身),您不会遇到这些伪像,因为该视图框会自动更新它覆盖的整个区域,但这不会改变您正在只是在你想要的地方画:这个项目还在另一个地方。

要验证这一点,只需在 paint() 末尾添加以下行:

    painter.save()
    painter.setPen(QtCore.Qt.white)
    painter.drawRect(self.boundingRect())
    painter.restore()

结果如下:

正如您在上图中所看到的,项目的实际矩形与您正在绘制的矩形非常不同,如果您单击新的矩形,您'将正确获取相关的鼠标事件。

现在,问题是 pyqtgraph 使用复杂的 QGraphicsItems 系统来显示其内容,addItem 实际上使用其转换和相对坐标系将项目添加到其内部 plotItem

如果你不需要与其他项目的直接关系和交互,并且你对固定位置没问题,一个可能性是继承 PlotWidget(它本身是一个 QGraphicsView 子类),并执行以下操作:

  • overwrite and override addItem(它被 PlotWidget 覆盖并包装到底层 PlotItem 对象方法),以便您可以将“可扩展”项添加到场景,而不是将它们添加到 PlotItem;为此,您还需要为可缩放项目创建对绘图项目的引用;
  • 向您的项目添加一个功能,该功能根据实际 视图大小(而不是视图框!)缩放自身,并根据视图框范围定位自身;
  • 覆盖 setPos 项目以保留对基于视框而不是场景的位置的引用;
  • 在 PlotItem 上安装事件过滤器以获取调整大小事件并最终 rescale/reposition 项目;
  • 将 PlotItem 的 sigRangeChanged 信号连接到实际调用上述函数的计时器(由于事件排队,此 被延迟,如即时调用会导致不可靠的结果);

这是上述的可能实现:

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()
# ...

注意:

  • 这仅在您只有一个视图时有效;虽然这对于 pyqtgraph 通常不是问题,但实际上可以在多个 QGraphicsView 中同时显示一个 QGraphicsScene,就像项目视图中的项目模型一样;
  • 要获取默认样式,不要创建新的 QWidget 实例,只需访问 QApplication style();
  • 空格对于代码的可读性非常重要(这通常比其他事情更重要,比如打字);阅读官方 Style Guide for Python Code(又名 PEP-8);