Search code examples
pythonpython-3.xpyqtpyqt5qgraphicsscene

How do you properly implement Snap To Grid Logic for a QGraphicsItem? Mine is not working


I am working on a project that requires me to create a custom QGraphicsItemGroup that snaps to a grid when a widget is checked. The snap to grid logic works (partially) as when I move the item, it moves 10 pixels per move (snapping). However, it moves from 0, 0 in the scene, meaning if the group is created at 100, 100, it will move as if it where created at 0, 0. I don't know if I really explained the problem well, so feel free to play with it yourself.

class CustomGraphicsItemGroup(QGraphicsItemGroup):
    def __init__(self, widget):
        super().__init__()

        self.widgets = widget

    def paint(self, painter, option, widget=None):
        # Call the parent class paint method first
        super().paint(painter, option, widget)

        # If the item is selected, draw a custom selection highlight
        if option.state & QStyle.State_Selected:
            pen = painter.pen()
            pen.setColor(QColor('#e00202'))
            pen.setWidth(2)
            pen.setCapStyle(Qt.RoundCap)
            painter.setPen(pen)
            painter.drawRect(self.boundingRect())

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
        if self.widgets.isChecked():
            block_size = 10

            # Calculate the position relative to the scene's coordinate system
            scene_pos = event.scenePos()
            x = int(scene_pos.x() / block_size) * block_size
            y = int(scene_pos.y() / block_size) * block_size

            # Set the position relative to the scene's coordinate system
            self.setPos(x, y)
            
        else:
            # Call the superclass's mouseMoveEvent to move the item as normal
            super().mouseMoveEvent(event)

I tried using different numbers, division, multiplication, and nothing worked. Any help or code snippits are appreciated.


Solution

  • So first off all, when inheriting the QGraphicsItemGroup, one should consider the concepts of it. The group does not want to have a child in the constructor. Instead, items are added to the group. The group itself is then a proxy for all added items. There are two ways to create a group in the scene. The option with which custom groups can be created in the scene is as follows.

    1. Create a group and add it to the scene.
    group = CustomGraphicsItemGroup()
    group.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
    group.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
    scene.addItem(group)
    
    1. Assign items to the group.
    group.addToGroup(red_item)
    group.addToGroup(blue_item)
    

    You should always use the tools that are available to Qt and not reinvent the wheel. It is important to set the right flags so that an item (or a group) is selectable and movable. Then use the self.isSelected of the item itself.

    To your question, an offset for the mouse must be applied somehow so that no sudden jumps occur. This can be stored in the mousePressEvent.

    import sys
    
    from PySide6.QtCore import QRect, QPoint
    from PySide6.QtGui import QColor, Qt
    from PySide6.QtWidgets import (
        QGraphicsItemGroup,
        QStyle,
        QGraphicsItem,
        QGraphicsScene,
        QGraphicsView,
        QApplication,
        QGraphicsRectItem,
    )
    
    
    class CustomGraphicsItemGroup(QGraphicsItemGroup):
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self.mouse_offset = QPoint(0, 0)
            self.block_size = 10
    
        def paint(self, painter, option, widget=None):
            # Call the parent class paint method first
            super().paint(painter, option, widget)
    
            # If the item is selected, draw a custom selection highlight
            if option.state & QStyle.State_Selected:
                pen = painter.pen()
                pen.setColor(QColor("#e00202"))
                pen.setWidth(2)
                pen.setCapStyle(Qt.RoundCap)
                painter.setPen(pen)
                painter.drawRect(self.boundingRect())
    
        def mousePressEvent(self, event):
            if event.button() == Qt.MouseButton.LeftButton:
                self.mouse_offset = event.pos()
            super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self.isSelected():
                # Calculate the position relative to the scene's coordinate system
                scene_pos = event.scenePos()
                x = (
                    int(scene_pos.x() / self.block_size) * self.block_size
                    - self.mouse_offset.x()
                )
                y = (
                    int(scene_pos.y() / self.block_size) * self.block_size
                    - self.mouse_offset.y()
                )
    
                # Set the position relative to the scene's coordinate system
                self.setPos(x, y)
            else:
                # Call the superclass's mouseMoveEvent to move the item as normal
                super().mouseMoveEvent(event)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        scene = QGraphicsScene()
        view = QGraphicsView(scene)
        scene.setSceneRect(QRect(0, 0, 500, 500))
        view.setFixedWidth(500)
        view.setFixedHeight(500)
        view.show()
    
        group = CustomGraphicsItemGroup()
        group.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
        group.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
        scene.addItem(group)
        red_item = QGraphicsRectItem()
        red_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
        red_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
        red_item.setBrush(QColor("red"))
        red_item.setRect(0, 0, 80, 80)
        red_item.setPos(150, 150)
        group.addToGroup(red_item)
    
        blue_item = QGraphicsRectItem()
        blue_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
        blue_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
        blue_item.setBrush(QColor("blue"))
        blue_item.setRect(0, 0, 100, 100)
        blue_item.setPos(100, 100)
        group.addToGroup(blue_item)
    
        scene.addItem(blue_item)
        scene.addItem(red_item)
        sys.exit(app.exec())