I have user-adjustable annotations in a graphics scene. The size/rotation of annotations is handled by dragging corners of a rectangle about the annotation. I'm using a custom rect (instead of the boundingRect) so it follows the rotation of the parent annotation. The control corners are marked by two ellipses whose parent is the rect so transformations of rect/ellipse/annotation are seamless.
I want to detect when the cursor is over one of the corners, which corner it is, and the exact coordinates. For this task it seems that I should filter the hoverevents with the parent rect using a sceneEventFilter.
I've tried umpty zilch ways of implementing the sceneEventFilter to no avail. All events go directly to the hoverEnterEvent function. I've only found a few bits of example code that do something like this but I'm just plain stuck. btw, I'm totally self taught on Python and QT over the past 3 months, so please bear with me. I'm sure I'm missing something very basic. The code is a simplified gui with two ellipses. We're looking to capture events in the sceneEventFilter but always goes to hoverEnterEvent.
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem
import sys
class myHandle(QtGui.QGraphicsEllipseItem):
def __init__(self, parent = None):
super(myHandle, self).__init__(parent)
def addTheHandle(self, h_parent = 'null', kind = 'null'):
handle_w = 40
if kind == 'scaling handle':
handle_x = h_parent.boundingRect().topRight().x() - handle_w/2
handle_y = h_parent.boundingRect().topRight().y() - handle_w/2
if kind == 'rotation handle':
handle_x = h_parent.boundingRect().topLeft().x() - handle_w/2
handle_y = h_parent.boundingRect().topLeft().y() - handle_w/2
the_handle = QtGui.QGraphicsEllipseItem(QtCore.QRectF(handle_x, handle_y, handle_w, handle_w))
the_handle.setPen(QtGui.QPen(QtGui.QColor(255, 100, 0), 3))
the_handle.setParentItem(h_parent)
the_handle.setAcceptHoverEvents(True)
the_handle.kind = kind
return the_handle
class myRect(QtGui.QGraphicsRectItem):
def __init__(self, parent = None):
super(myRect, self).__init__(parent)
def rectThing(self, boundingrectangle):
self.setAcceptHoverEvents(True)
self.setRect(boundingrectangle)
mh = myHandle()
rotation_handle = mh.addTheHandle(h_parent = self, kind = 'rotation handle')
scaling_handle = mh.addTheHandle(h_parent = self, kind = 'scaling handle')
self.installSceneEventFilter(rotation_handle)
self.installSceneEventFilter(scaling_handle)
return self, rotation_handle, scaling_handle
def sceneEventFilter(self, event):
print('scene ev filter')
return False
def hoverEnterEvent(self, event):
print('hover enter event')
class Basic(QtGui.QMainWindow):
def __init__(self):
super(Basic, self).__init__()
self.initUI()
def eventFilter(self, source, event):
return QtGui.QMainWindow.eventFilter(self, source, event)
def exit_the_program(self):
pg.exit()
def initUI(self):
self.resize(300, 300)
self.centralwidget = QtGui.QWidget()
self.setCentralWidget(self.centralwidget)
self.h_layout = QtGui.QHBoxLayout(self.centralwidget)
self.exit_program = QtGui.QPushButton('Exit')
self.exit_program.clicked.connect(self.exit_the_program)
self.h_layout.addWidget(self.exit_program)
self.this_scene = QGraphicsScene()
self.this_view = QGraphicsView(self.this_scene)
self.this_view.setMouseTracking(True)
self.this_view.viewport().installEventFilter(self)
self.h_layout.addWidget(self.this_view)
self.circle = self.this_scene.addEllipse(QtCore.QRectF(40, 40, 65, 65), QtGui.QPen(QtCore.Qt.black))
mr = myRect()
the_rect, rotation_handle, scaling_handle = mr.rectThing(self.circle.boundingRect())
the_rect.setPen(QtGui.QPen(QtCore.Qt.black))
the_rect.setParentItem(self.circle)
self.this_scene.addItem(the_rect)
self.this_scene.addItem(rotation_handle)
self.this_scene.addItem(scaling_handle)
def main():
app = QtGui.QApplication([])
main = Basic()
main.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The main problem is that you are installing the event filter of the target items on the rectangle: the event filter of the rectangle will never receive anything. Moreover, sceneEventFilter
accepts two arguments (the watched item and the event), but you only used one.
What you should do is to install the event filter of the rectangle on the target items:
rotation_handle.installSceneEventFilter(self)
scaling_handle.installSceneEventFilter(self)
That said, if you want to use those ellipse items for scaling or rotation of the source circle, your approach is a bit wrong to begin with.
from math import sqrt
# ...
class myRect(QtGui.QGraphicsRectItem):
def __init__(self, parent):
super(myRect, self).__init__(parent)
self.setRect(parent.boundingRect())
# rotation is usually based on the center of an object
self.parentItem().setTransformOriginPoint(self.parentItem().rect().center())
# a rectangle that has a center at (0, 0)
handleRect = QtCore.QRectF(-20, -20, 40, 40)
self.rotation_handle = QtGui.QGraphicsEllipseItem(handleRect, self)
self.scaling_handle = QtGui.QGraphicsEllipseItem(handleRect, self)
# position the handles by centering them at the right corners
self.rotation_handle.setPos(self.rect().topLeft())
self.scaling_handle.setPos(self.rect().topRight())
for source in (self.rotation_handle, self.scaling_handle):
# install the *self* event filter on the handles
source.installSceneEventFilter(self)
source.setPen(QtGui.QPen(QtGui.QColor(255, 100, 0), 3))
def sceneEventFilter(self, source, event):
if event.type() == QtCore.QEvent.GraphicsSceneMouseMove:
# map the handle event position to the ellipse parent item; we could
# also map to "self", but using the parent is more consistent
localPos = self.parentItem().mapFromItem(source, event.pos())
if source == self.rotation_handle:
# create a temporary line to get the rotation angle
line = QtCore.QLineF(self.boundingRect().center(), localPos)
# add the current rotation to the angle between the center and the
# top left corner, then subtract the new line angle
self.parentItem().setRotation(135 + self.parentItem().rotation() - line.angle())
# note that I'm assuming that the ellipse is a circle, so the top
# left angle will always be at 135°; if it's not a circle, the
# rect width and height won't match and the angle will be
# different, so you'll need to compute that
# parentRect = self.parentItem().rect()
# oldLine = QtCore.QLineF(parentRect.center(), parentRect.topLeft())
# self.parentItem().setRotation(
# oldLine.angle() + self.parentItem().rotation() - line.angle())
elif source == self.scaling_handle:
# still assuming a perfect circle, so the rectangle is a square;
# the line from the center to the top right corner is used to
# compute the square side size, which is the double of a
# right-triangle cathetus where the hypotenuse is the line
# between the center and any of its corners;
# if the ellipse is not a perfect circle, you'll have to
# compute both of the catheti
hyp = QtCore.QLineF(self.boundingRect().center(), localPos)
size = sqrt(2) * hyp.length()
rect = QtCore.QRectF(0, 0, size, size)
rect.moveCenter(self.rect().center())
self.parentItem().setRect(rect)
self.setRect(rect)
# update the positions of both handles
self.rotation_handle.setPos(self.rect().topLeft())
self.scaling_handle.setPos(self.rect().topRight())
return True
elif event.type() == QtCore.QEvent.GraphicsSceneMousePress:
# return True to the press event (which is almost as setting it as
# accepted, so that it won't be processed further more by the scene,
# allowing the sceneEventFilter to capture the following mouseMove
# events that the watched graphics items will receive
return True
return super(myRect, self).sceneEventFilter(source, event)
class Basic(QtGui.QMainWindow):
# ...
def initUI(self):
# ...
self.circle = self.this_scene.addEllipse(QtCore.QRectF(40, 40, 65, 65), QtGui.QPen(QtCore.Qt.black))
mr = myRect(self.circle)
self.this_scene.addItem(mr)