user17796485
user17796485

Reputation:

How to limit the area in QGraphicsScene where a custom QGraphicsItem can be moved?

I have a QGraphicsScene where I have QGraphicsItems. These items are movable and I can move them all over the QGraphicsScene but I would like to limit the area where these items can be moved. The sizes of the QGraphicsScene don't have to change. I would really appreciate if someone gave me an example of how to do it in python.

Here's what I have now

from PySide2.QtCore import QPointF
from PySide2.QtWidgets import QWidget, QVBoxLayout, QGraphicsView, \
    QGraphicsScene, QGraphicsPolygonItem, QApplication
from PySide2.QtGui import QPen, QColor, QBrush, QPolygonF


class Test(QWidget):
    def __init__(self, parent=None):
        super(Test, self).__init__(parent)
        self.resize(1000, 800)
        self.layout_ = QVBoxLayout()
        self.view_ = GraphicsView()

        self.layout_.addWidget(self.view_)
        self.setLayout(self.layout_)

class GraphicsView(QGraphicsView):
    def __init__(self):
        super(GraphicsView, self).__init__()
        self.scene_ = QGraphicsScene()
        self.polygon_creation = self.PolyCreation()

        self.scene_.setSceneRect(0, 0, 400, 400)
        self.setScene(self.scene_)

        self.polyCreator()

    def polyCreator(self):
        self.polygon_creation.poly()
        polygon = self.polygon_creation.polygon()
        new_poly = self.scene().addPolygon(polygon)
        new_poly.setBrush(QBrush(QColor("gray")))
        new_poly.setPen(QPen(QColor("gray")))
        new_poly.setFlag(QGraphicsPolygonItem.ItemIsSelectable)
        new_poly.setFlag(QGraphicsPolygonItem.ItemIsMovable)
        new_poly.setFlag(QGraphicsPolygonItem.ItemIsFocusable)
        new_poly.setPos(0, 0)

    class PolyCreation(QGraphicsPolygonItem):
        def __init__(self):
            super().__init__()
            self.setAcceptHoverEvents(True)

        def poly(self):
            self.poly_points = (QPointF(0, 0),
                                      QPointF(0, 50),
                                      QPointF(50, 50),
                                      QPointF(50, 0))

            self.shape = QPolygonF(self.poly_points)
            self.setPolygon(self.shape)


if __name__ == '__main__':
    app = QApplication([])
    win = Test()
    win.show()
    app.exec_()

I've also found an answer in cpp, but I can't understand it very well, so if someone could "translate" it in python that'd be great too. Here's the link restrict movable area of qgraphicsitem (Please check @Robert's answer)

Upvotes: 1

Views: 403

Answers (2)

Kamin
Kamin

Reputation: 333

By reimplementing the mouseMoveEvent method, items can be easily restricted from leaving the sceneRect:

def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):

    # 1. Finding the maximum movements that can be made before leaving the scene's
    # bounding rectangle:
    rect = self.mapRectToScene(self.boundingRect())
    max_left_move = self.scene().sceneRect().left() - min(rect.left(), rect.right())
    max_right_move = self.scene().sceneRect().right() - max(rect.left(), rect.right())
    max_up_move = self.scene().sceneRect().top() - min(rect.top(), rect.bottom())
    max_down_move = self.scene().sceneRect().bottom() - max(rect.top(), rect.bottom())
    
    # 2. Calculating the initial vector of movement
    move = event.pos() - event.buttonDownPos(Qt.LeftButton)
    
    # 3. Correcting the movement vector if it causes the item to leave the area:
    move.setX(np.clip(move.x(), max_left_move, max_right_move))
    move.setY(np.clip(move.y(), max_up_move, max_down_move))
    
    # 4. Moving the item
    self.moveBy(move.x(), move.y())

Since I needed to restrict items with transformations, the method that @musicamante offered was not appropriate for my purpose.

Upvotes: 0

musicamante
musicamante

Reputation: 48231

The concept is to restrict the new position before it's finally applied.

To achieve so, you need to also set the ItemSendsGeometryChanges flag and check for ItemPositionChange changes, then compare the item bounding rect with that of the scene, and eventually return a different position after correcting it.

class PolyCreation(QGraphicsPolygonItem):
    def __init__(self):
        super().__init__(QPolygonF([
            QPointF(0, 0),
            QPointF(0, 50),
            QPointF(50, 50),
            QPointF(50, 0)
        ]))
        self.setBrush(QBrush(QColor("gray")))
        self.setPen(QPen(QColor("blue")))
        self.setFlags(
            self.ItemIsSelectable
            | self.ItemIsMovable
            | self.ItemIsFocusable
            | self.ItemSendsGeometryChanges
        )
        self.setAcceptHoverEvents(True)

    def itemChange(self, change, value):
        if change == self.ItemPositionChange and self.scene():
            br = self.polygon().boundingRect().translated(value)
            sceneRect = self.scene().sceneRect()
            if not sceneRect.contains(br):
                if br.right() > sceneRect.right():
                    br.moveRight(sceneRect.right())
                if br.x() < sceneRect.x():
                    br.moveLeft(sceneRect.x())
                if br.bottom() > sceneRect.bottom():
                    br.moveBottom(sceneRect.bottom())
                if br.y() < sceneRect.y():
                    br.moveTop(sceneRect.top())
                return br.topLeft()
        return super().itemChange(change, value)


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

        self.scene_.setSceneRect(0, 0, 400, 400)
        self.setScene(self.scene_)
        self.scene_.addItem(PolyCreation())

Notes:

  • the above code will obviously only work properly for top level items (not children of other items);
  • it will work as long as the item doesn't have any transformation applied (rotation, scale, etc.); if you want to support that, you have to consider the sceneTransform() to get the actual bounding rect of the polygon;
  • it doesn't consider the pen width, so if the item has a thick pen, the resulting polygon may go beyond the scene boundaries; to avoid that, use the actual boundingRect() of the item and adjust it by using half the pen width;
  • avoid nested classes, they are rarely required and they only tend to make code unnecessarily convoluted;
  • you were not actually using that subclass, since you're in fact adding another polygon item based on the polygon of that instance;
  • all items are always positioned at (0, 0) by default, specifying it again is pointless;
  • shape() is an existing (and quite important) function of all items, you shall not overwrite it;

Upvotes: 2

Related Questions