Search code examples
pythonmouseeventmultipleselectionpyside6

Retain QGraphicsItem multi-selection movement after creating mouse events


I am working on a click-and-drag map editor with Python Qt using QGraphicsRectItems created dynamically. I needed to add 3 mouse event functions in the QGraphicsRectItem class in order for those rectangles to automatically snap to a 25x15 grid when the mouse is released after moving the item around which works just fine.

The problem is that after adding those functions I no longer have the ability to move multiple rectangles at once when selected. I can still select several rectangles at the same time by dragging my mouse over them but trying to move the whole selection will only move one of the items.

Here is a sample of my code:

import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *

TILEWIDTH = 25
TILEHEIGHT = 15
OUTLINE = 3

KEY_METADATA = 1

class RoomItem(QGraphicsRectItem):
    def __init__(self, offset_x, offset_y, width, height, outline, fill, metadata=None, parent=None):
        super().__init__(0, 0, width, height, parent)
        self.setPos(offset_x, offset_y)
        self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable)
        self.setData(KEY_METADATA, metadata)
        
        self.setPen(outline)
        self.setBrush(fill)

    #Mouse functions to snap rectItem to grid
    
    #Get mouse and rect positions on the initial click
    def mousePressEvent(self, event):
        self.click_x = event.scenePos().x()
        self.click_y = event.scenePos().y()
        self.initial_x = self.pos().x()
        self.initial_y = self.pos().y()
    
    #Move rectangle relative to the mouse
    def mouseMoveEvent(self, event):
        x = event.scenePos().x() - (self.click_x - self.initial_x)
        y = event.scenePos().y() - (self.click_y - self.initial_y)
        pos = QPointF(x, y)
        self.setPos(pos)
    
    #Snap rectangle to 25x15 grid when mouse is released
    def mouseReleaseEvent(self, event):
        x = round((event.scenePos().x() - (self.click_x - self.initial_x))/TILEWIDTH)*TILEWIDTH
        y = round((event.scenePos().y() - (self.click_y - self.initial_y))/TILEHEIGHT)*TILEHEIGHT
        pos = QPointF(x, y)
        self.setPos(pos)

class Main(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.scene = QGraphicsScene(self)
        self.view = QGraphicsView(self.scene, self)
        
        self.view.setDragMode(QGraphicsView.RubberBandDrag)
        self.view.scale(1, -1)
        self.view.setStyleSheet("background:transparent; border: 0px")
        self.setCentralWidget(self.view)
  
    def draw_map(self):
        #Drawing from an existing list
        for i in self.room_list:
            fill = QColor("#000000")
            outline = QPen("#ffffff")
            outline.setWidth(OUTLINE)
            outline.setJoinStyle(Qt.MiterJoin)
            
            #Creating the RoomItem
            rect = RoomItem(i.offset_x, i.offset_z, i.width, i.height, outline, fill)
            self.scene.addItem(rect)

def main():
    app = QApplication(sys.argv)
    main = Main()
    main.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

How can I get back the multi-select-and-move system that was set by default while retaining my mouse functions that allow each object to snap to the grid ?


Solution

  • If you want the item positions to be multiples of certain integer values then you must correct it after releasing the mouse, it is not necessary to override the mousePressEvent and mouseReleaseEvent method since in your implementation you are modifying the default functionality which in this case is to move several items at once.

    import random
    import sys
    
    from PySide6.QtCore import Qt
    from PySide6.QtGui import QColor, QPen
    from PySide6.QtWidgets import (
        QApplication,
        QGraphicsItem,
        QGraphicsRectItem,
        QGraphicsScene,
        QGraphicsView,
        QMainWindow,
    )
    
    TILEWIDTH = 25
    TILEHEIGHT = 15
    OUTLINE = 3
    
    KEY_METADATA = 1
    
    
    def round_by_factor(value, factor):
        return round(value / factor) * factor
    
    
    class RoomItem(QGraphicsRectItem):
        def __init__(
            self,
            offset_x,
            offset_y,
            width,
            height,
            outline,
            fill,
            metadata=None,
            parent=None,
        ):
            super().__init__(0, 0, width, height, parent)
            self.setPos(offset_x, offset_y)
            self.setFlags(
                QGraphicsItem.ItemIsSelectable
                | QGraphicsItem.ItemIsFocusable
                | QGraphicsItem.ItemIsMovable
            )
            self.setData(KEY_METADATA, metadata)
    
            self.setPen(outline)
            self.setBrush(fill)
    
        def mouseReleaseEvent(self, event):
            super().mouseReleaseEvent(event)
            for item in self.scene().selectedItems():
                self.apply_round(item)
    
        def apply_round(self, item):
            x = round_by_factor(item.pos().x(), TILEWIDTH)
            y = round_by_factor(item.pos().y(), TILEHEIGHT)
            item.setPos(x, y)
    
    
    class Main(QMainWindow):
        def __init__(self):
            super().__init__()
            self.initUI()
    
        def initUI(self):
            self.scene = QGraphicsScene(self)
            self.view = QGraphicsView(self.scene, self)
            self.view.setDragMode(QGraphicsView.RubberBandDrag)
            self.view.scale(1, -1)
            self.view.setStyleSheet("background:transparent; border: 0px")
            self.setCentralWidget(self.view)
    
            self.draw_map()
    
        def draw_map(self):
            for _ in range(10):
                fill = QColor("#000000")
                outline = QPen("#ffffff")
                outline.setWidth(OUTLINE)
                outline.setJoinStyle(Qt.MiterJoin)
    
                offset_x = TILEWIDTH * random.randint(-10, 10)
                offset_z = TILEHEIGHT * random.randint(-10, 10)
                width = TILEWIDTH * random.randint(2, 4)
                height = TILEHEIGHT * random.randint(2, 4)
                rect = RoomItem(offset_x, offset_z, width, height, outline, fill)
                self.scene.addItem(rect)
    
    
    def main():
        app = QApplication(sys.argv)
        main = Main()
        main.show()
        sys.exit(app.exec())
    
    
    if __name__ == "__main__":
        main()