鼠标事件检测与 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);
我修改了像素图项目的绘制方法,因此它总是按小部件高度的百分比进行绘制,并以其放置位置的 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);