【问题标题】:Mouse Event Detection inconsistent with customized QGraphicsItem in pyqtgraph plot鼠标事件检测与 pyqtgraph 图中的自定义 QGraphicsItem 不一致
【发布时间】:2022-01-13 00:20:53
【问题描述】:

我修改了像素图项的绘制方法,使其始终按小部件高度的百分比进行绘制,并以它所在位置的 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_()

【问题讨论】:

    标签: python qt pyqt5 pyqtgraph qgraphicsitem


    【解决方案1】:

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

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

    确保将所有绘画限制在 boundingRect() 的边界内以避免呈现伪影(因为 QGraphicsView 不会为您剪辑画家)。

    由于 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 对象方法),以便您可以将“可缩放”项目添加到场景中,而不是将它们添加到绘图项;这样做,您还需要为可扩展项目创建对绘图项目的引用;
    • 为您的项目添加一个函数,该函数根据实际视图大小(不是视图框!)自行缩放,并根据视图框范围定位自身;
    • 覆盖项目的setPos 以保持对基于视图框的位置的引用,而不是场景的引用;
    • 在 PlotItem 上安装事件过滤器以获取调整大小事件并最终重新调整/重新定位项目;
    • 将 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 的问题,但实际上可以在多个 QGraphicsViews 中同时显示一个 QGraphicsScene,就像项目视图中的项目模型一样;
    • 要获取默认样式,不要新建QWidget实例,直接访问QApplicationstyle()即可;
    • 空格对于代码的可读性非常很重要(这通常比其他事情更重要,比如打字);在官方Style Guide for Python Code(又名 PEP-8)上阅读更多信息;

    【讨论】:

    • 感谢您的出色回答! 1) 如果在没有更新比例的情况下调用 setPos,则移位将无法进行。我应该使用 setOffset 来应用 x-shift 吗? 2)我有点意识到这是一个边界框问题,并尝试覆盖 boundingBox() 但没有找到正确的实现。这是一个值得的方法吗?任何关于按照我原来的思路正确实现 boundingBox() 的提示?如果我确实覆盖 boundingBox,我可能会遭受很大的性能损失吗?
    • @Techniquab 1) 对不起,我不明白你的意思,你能试着解释一下吗? 2)我不建议覆盖boundingRect(),因为我认为它会使事情变得更丑陋(而且不容易),最重要的是因为你需要在python方面实现它:设置新的像素图和位置可能会添加一些开销很小,但这主要是在 C++ 端完成的(所以,更好、更容易、更快):只要您可以依赖默认实现,就应该使用它:使用我的方法,您已经(间接)这样做了,所以我认为从 python 中做这件事没有意义。
    猜你喜欢
    • 1970-01-01
    • 2020-09-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-22
    • 1970-01-01
    • 1970-01-01
    • 2018-12-10
    • 1970-01-01
    相关资源
    最近更新 更多