Grid snapping a QGraphicsItemGroup subclass

43 Views Asked by At

I am trying to make a custom subclass of QGraphicsItemGroup which snaps to a grid on a QGraphicsScene. I recently posted a question about this. My first problem was that I could not move the item groups to the left or right of their original position. I learned that I was forgetting that the initial position of an item is always (0, 0), and so I was able to fix this by setting the position prior to adding items to the group. So the problem is solved for a single movement.

In my actual program, there are various objects on the scene and I want to be able to change which ones are being moved as a group. My approach is to create a new group when the mouse is pressed consisting of the objects I want to move, then destroy the group when the mouse is released. Here is a minimal working example:

from os import environ

environ["QT_ENABLE_HIGHDPI_SCALING"] = "0"

from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *

app = QApplication([])

class Location(QGraphicsItem):
    color = QColor(0, 110, 200, 75)
    border_color = QColor(0, 255, 0, 255)

    def __init__(self, size, **kwargs):
        super().__init__(**kwargs)
        self.width, self.height = size
        
    # def boundingRect(self):
    #     return QRectF(0, 0, 32*self.width, 32*self.height)

    def paint(self, *args):
        painter = args[0]
        painter.fillRect(1, 1, int(32*self.width) - 2, int(32*self.height) - 2, Location.color)
        
        pen = QPen(Location.border_color)
        pen.setWidth(0)
        painter.setPen(pen)
        painter.drawRect(0, 0, int(32*self.width) - 1, int(32*self.height) - 1)
        
    def x(self):
        """Returns the x-coordinate of the location's position."""
        return int(self.pos().x())
    
    def y(self):
        """Returns the y-coordinate of the location's position."""
        return int(self.pos().y())
        

class GridSnappingQGIG(QGraphicsItemGroup):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
        
    def boundingRect(self):
        return self.childrenBoundingRect()

    def itemChange(self, change, value):
        scene = self.scene()
        if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and scene:
            x, y = scene.snap_to_grid(value.x(), value.y(), scene.grid_size)
            return QPointF(x, y)
        return super().itemChange(change, value)


class Scene(QGraphicsScene):

    def __init__(self, *args):
        super().__init__(*args)
        self.grid_size = 32
        self.locations = []

    def snap_to_grid(self, x, y, grid_size):
        return [grid_size * round(x / grid_size), grid_size * round(y / grid_size)]
                
    def place_location(self, x, y, size):
        """Places a location of dimensions size at position (x, y)."""
        loc = Location(size)
        self.locations.append(loc)
        self.addItem(loc)
        loc.setPos(x, y)
                
    def move_locations(self, locations):
        """Puts the locations in the input list locations in a GridSnappingQGIG to move them."""
        x, y = min([loc.x() for loc in locations]), min([loc.y() for loc in locations])
        group = GridSnappingQGIG()
        self.addItem(group)
        group.setPos(x, y)
        
        for loc in locations:
            group.addToGroup(loc)
                
    def mousePressEvent(self, event):
        self.move_locations(self.locations)
        QGraphicsScene.mousePressEvent(self, event)
        
    def mouseReleaseEvent(self, event):
        group = self.locations[0].group()
        for loc in self.locations:
            group.removeFromGroup(loc)
        self.removeItem(group)
        QGraphicsScene.mouseReleaseEvent(self, event)

scene = Scene(0, 0, 480, 480)
scene.setBackgroundBrush(QColor(0, 0, 0))

scene.place_location(128, 128, [2,2])
scene.place_location(64, 64, [2,2])

frame = QFrame()
window = QMainWindow()
view = QGraphicsView(scene)

layout = QVBoxLayout()
layout.addWidget(view)
frame.setLayout(layout)

window.setCentralWidget(frame)
window.showMaximized()    
app.exec()

The problem is two-fold: Firstly, if you try to move the locations a second time, the newly created group jumps wildly past the mouse cursor. Secondly, the program is pretty prone to crashing with no error message. I suspect both problems are due to some mismanagement of memory (i.e. Python still holds a reference to some C++ object that has been deleted or vice versa), as I know that is a common cause of crashes in PyQt6 and I don't see how else the movement operation would behave differently a second time.

EDIT: Per the comments, getting rid of the boundingRect override in the Scene class solves the crashing problem, but not the jumpiness problem. I reduced the scene size from 8192x8192 to 480x480 and removed boundary checking.

0

There are 0 best solutions below