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.