How can I draw a QGraphicsRectItem that is (properly) movable by clicking on a QGraphicsView/QGraphicsScene canvas?

Question:

I want to draw rectangles on a QGrahicsView/QGrahicsScene canvas by subclassing QGraphicsRectItem. The rectangles should appear just by clicking on the canvas. After drawing the rectangles, the rectangles should be movable just by dragging them around.

  1. I subclassed QGraphicsRectItem to draw my custom rectangle and set it as movable via .setFlags(QGraphicsItem.ItemIsMovable).
  2. I subclassed QGraphicsView and implemented mousePressEvent() such that it instantiates my custom QGraphicsRectItem class with the coordinates of the clicked position.

I expect to be able to draw rectangles on the canvas just by clicking on it. Furthermore, I want to be able to move the rectangles around. I can do both, but only drawing the rectangles by clicking on the canvas works properly. Grabbing the rectangles to move them around is next to impossible because I have to grab the exact pixel (actually, it’s the top-left intersection between the rectangle lines) to move the rectangles around.

I can grab a rectangle properly by clicking anywhere on its body if I do not spawn a rectangle by reimplementing mousePressEvent() but instead just instantiate it in the class’s initiator. But then I lose the ability to spawn rectangles just by clicking on the canvas.

Is there a possibility to keep the functionality of spawning rectangles just by clicking on the canvas while also being able to easily move the rectangles around?

This is what I have got so far:

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QMouseEvent, QPen
from PyQt5.QtWidgets import (
    QApplication,
    QGraphicsItem,
    QGraphicsRectItem,
    QGraphicsScene,
    QGraphicsView,
)


class RectangleItem(QGraphicsRectItem):
    def __init__(self, x: float, y: float, width: float, height: float):
        super().__init__(x, y, width, height)

        self.setBrush(QBrush(Qt.red))
        self.setPen(QPen(Qt.black, 20))

        self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)


class GraphicsView(QGraphicsView):
    def __init__(self, scene: QGraphicsScene):
        super().__init__(scene)

        # rectangle = RectangleItem(0, 0, 100, 100)
        # self.scene().addItem(rectangle)

    def mousePressEvent(self, event: QMouseEvent):
        scene_position = self.mapToScene(event.pos())
        rectangle = RectangleItem(scene_position.x(), scene_position.y(), 100, 100)
        self.scene().addItem(rectangle)


app = QApplication([])
scene = QGraphicsScene()
view = GraphicsView(scene)
view.show()
app.exec()
Asked By: eraintash

||

Answers:

Thanks to the hint of @Homer521 and a bit of research, I found a solution:

Instead of my above reimplementation of mousePressEvent, I used

def mousePressEvent(self, event: QMouseEvent):
    item = self.itemAt(event.pos())
    if item:
        return super().mousePressEvent(event)
    scene_position = self.mapToScene(event.pos())
    rectangle = RectangleItem(scene_position.x(), scene_position.y(), 100, 100)
    self.scene().addItem(rectangle)
    return None

Thsi first checks whether a QGraphicsItem (or a subclass of it) exists at the position you clicked at. If it does, it just returns to the original implementation of mousePressEvent which allows you to use all the mouse interaction related functionality that a QGraphicsItem comes with by default. If there is no item at that position, it just runs the code responsible for generating the function.

Bonus: if you want to be able to use context menus on your QGraphicsItems, wrap the above definition of mousePressEvent in a if event.button() == Qt.LeftButton: so that it is only executed when you left click.

Answered By: eraintash
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.