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_()
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 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).
Since pyqtgraph adds items to its viewbox (a QGraphicsItem subclass itself), you're not experiencing those artifacts because that viewbox automatically updates the whole area covered by it, but that doesn't change the fact that you're just painting where you want it to: the item is still in another place.
To verify that, just add the following lines at the end of paint()
:
painter.save()
painter.setPen(QtCore.Qt.white)
painter.drawRect(self.boundingRect())
painter.restore()
The result will be the following:
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:
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;setPos
of your item to keep a reference to the position based on the viewbox, instead of that of the scene;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:
style()
;