Search code examples
pythonc++qtpyqtqgraphicsitem

Cannot properly position QGraphicsRectItem in scene


I cannot figure this out for the life of me, but I've boiled this down to a self contained problem.

What I am trying to do, is draw a QGraphicsRectItem around the items that are selected in a QGraphicsScene. After the rect is drawn it can be moved in a way that moves all of the items together. I've looked into QGraphicsItemGroup already and decided it is not feasible in my real use case.

The problem: I've been able to accomplish everything mentioned above, except I can't get the rect item to be positioned properly i.e. it is the right size and by moving it all items are moved but it is not lined up with the united bounding rect of the selected items. I've tried to keep everything in scene coordinates so I'm not sure why there is an offset.

Why does there appear to be an offset and how can this be mitigated?

Here is the runnable code that can be tested by ctrl-clicking or rubber band selection (I know this is a good amount of code but the relevant sections are commented).

#####The line where the position of the rect item is set is marked like this#####

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys


class DiagramScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.selBox = None
        self.selectionChanged.connect(self.onSelectionChange)

    @pyqtSlot()
    def onSelectionChange(self):
        count = 0
        items = self.selectedItems()

        # Get bounding rect of all selected Items
        for item in self.selectedItems():
            if count == 0:
                rect = item.mapRectToScene(item.boundingRect())
            else:
                rect = rect.unite(item.mapRectToScene(item.boundingRect()))
            count += 1

        if count > 0:
            if self.selBox:
                # Update selBox if items are selected and already exists
                self.selBox.setRect(rect)
                self.selBox.items = items
            else:
                # Instantiate selBox if items are selected and does not already exist
                self.selBox = DiagramSelBox(rect, items)
                ##### Set position of selBox to topLeft corner of united rect #####
                self.selBox.setPos(rect.topLeft())
                self.addItem(self.selBox)
        elif self.selBox:
            # Remove selBox from scene if no items are selected and box is drawn
            self.removeItem(self.selBox)
            del self.selBox
            self.selBox = None


class DiagramSelBox(QGraphicsRectItem):
    def __init__(self, bounds, items, parent=None, scene=None):
        super().__init__(bounds, parent, scene)

        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.pressPos = None
        self.items = items

    def paint(self, painter, option, widget=None):
        pen = QPen(Qt.DashLine)
        painter.setPen(pen)
        painter.drawRect(self.rect())

    def mousePressEvent(self, e):
        # Get original position of selBox when clicked
        self.pressPos = self.pos()
        # mouseEvent is not passed on to scene so item selection
        # does not change

    def mouseMoveEvent(self, e):
        super().mouseMoveEvent(e)
        if self.pressPos:
            # Move selBox is original position is set
            newPos = self.mapToScene(e.pos()) - self.rect().center()
            self.setPos(newPos)

    def mouseReleaseEvent(self, e):
        # Update position of all selected items
        change = self.scenePos() - self.pressPos
        for item in self.items:
            item.moveBy(change.x(), change.y())

        super().mouseReleaseEvent(e)


if __name__ == "__main__":

    app = QApplication(sys.argv)

    view = QGraphicsView()
    view.setDragMode(QGraphicsView.RubberBandDrag)

    scene = DiagramScene()

    scene.setSceneRect(0, 0, 500, 500)

    rect1 = scene.addRect(20, 20, 100, 50)
    rect2 = scene.addRect(80, 80, 100, 50)
    rect3 = scene.addRect(140, 140, 100, 50)

    rect1.setFlag(QGraphicsItem.ItemIsSelectable, True)
    rect2.setFlag(QGraphicsItem.ItemIsSelectable, True)
    rect3.setFlag(QGraphicsItem.ItemIsSelectable, True)

    view.setScene(scene)
    view.show()

    sys.exit(app.exec_())

Solution

  • I don't have PyQt installed, but I've run into similar issues with the regular QT and QGraphicsRectItem.

    I think you've mixed some things up regarding the coordinate system. The bounding-rect of every QGraphicsItem is in local coordinates. The Point (0,0) in local-coordinates appears at the scene on the coordinates given by QGraphicsItem::pos() (scene-coordiantes).

    QGraphicsRectItem is a bit special, because we normally don't touch pos at all (so we leave it at 0,0) and pass a rect in scene-coordinates to setRect. QGraphicsRectItem::setRect basically set's the bounding rect to the passed value. So if you don't call setPos (in onSelectionChange) at all, and only pass scene-coordinates to setRect you should be fine.

    The mouseEvents in DiagramSelBox need to be adjusted as well. My approach would look like this:

    1. mousePress: store the difference between e.pos (mapped to scene) and self.rect.topLeft() in self.diffPos and copy self.rect.topLeft to self.startPos
    2. mouseMove: ensure that the difference between e.pos (mapped to scene) and self.rect.topLeft() stays the same, by moving self.rect around (use self.diffPos for the calculation)
    3. mouseRelease: move the items by the difference between self.rect.topLeft() and self.startPos.

    Hope that helps to get you started.