Search code examples
pythonpyqtpyqt5qgraphicssceneqgraphicsrectitem

QGraphicsItem not staying in place after being moved


I'm currently creating an application that uses QGraphicsView and allows the user to move QGraphicsItems around, so they can create diagram-like structures.

I need the items to change color when clicked but change back to their original color when the mouse button is released. However, when I define the "mouseReleaseEvent()" method, the item just revert back to the original position when I click anywhere on the viewport after moving it.

How can I make the Item stay in place after moving it the first time?

In order to have more control over the item positioning, I tried using "setSceneRect()" for the scene, but it did not solve the problem.

I couldn't solve the problem using the CustomItem's "setPos()" method either. It seems the coordinate system changes when there are multiple items on the same scene.

An additional issue is that some other items should change color when the mouse is over them. I tried overriding the "hoverEnterEvent()" method in the CustomItem class, but it isn't working.

Here is a minimal code for reproducing the problem I'm facing.

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        self.setupUi(self)

        self.main_menu = self.menuBar().addMenu("&Menu") 
        self.addItem = QtWidgets.QAction("&Add Rectangle", self, triggered = self.addRectangle)
        self.delItem = QtWidgets.QAction("&Delete Selected Rectangle(s)", self, triggered = self.delRectangle) 
        self.main_menu.addAction(self.addItem) 
        self.main_menu.addAction(self.delItem) 


        self.scene = CustomScene(self.main_menu) 
        self.graphicsView.setScene(self.scene)

    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.graphicsView = QtWidgets.QGraphicsView(self.centralwidget)
        self.graphicsView.setObjectName("graphicsView")
        self.verticalLayout.addWidget(self.graphicsView)
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))

    def addRectangle(self):
        self.item = CustomItem()
        self.scene.addItem(self.item)
        self.scene.update()

    def delRectangle(self):
        for item in self.scene.selectedItems():
            self.scene.removeItem(item)

class CustomScene (QtWidgets.QGraphicsScene):

    def __init__(self, scene_menu, parent=None):
        super(CustomScene, self).__init__(parent)

        self.setSceneRect(0,0,750,500)
        self.sceneMenu = scene_menu

    def contextMenuEvent (self, event):
        self.sceneMenu.exec_(event.screenPos()) 

class CustomItem (QtWidgets.QGraphicsRectItem):

    def __init__(self, parent = None, scene = None):
        super(CustomItem, self).__init__()

        self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) 
        self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) 

        self.setRect(200,200,120,25) #Creates and show the rectangle

    def contextMenuEvent(self, event): #Defines the menu shown on mouse right-click
        self.scene().clearSelection()
        self.setSelected(True)
        self.RCMenu.exec_(event.screenPos()) 

    def mousePressEvent(self, event):
        self.setBrush(QtGui.QBrush(QtCore.Qt.cyan))

    def hoverEnterEvent(self, event): #Not Working as intended, but it should change the rectangle's color to lightGray when I hover the mouse over it
        self.setBrush(QtGui.QBrush(QtCore.Qt.lightGray))

    def mouseReleaseEvent(self, event): # HERE IS THE REAL PROBLEM. WHENEVER I CLICK ON THE RECTANGLE AFTER IT'S RELEASE, IT GOES TO A SEEMINGLY RANDOM LOCATION
        self.setBrush(QtGui.QBrush(QtCore.Qt.white))

if __name__ == "__main__":
    import sys

    if not QtWidgets.QApplication.instance():
        app = QtWidgets.QApplication(sys.argv)

    else:
        app = QtWidgets.QApplication.instance()

    window = Ui_MainWindow()
    window.show()
    sys.exit(app.exec_())

Solution

  • The QGraphicsItem already has the behavior of moving to a new position, but when you overwrite these behaviors since you are deleting the implementation of the parent class, if you want to keep the behavior you must call the method of the parent class through super .

    On the other hand you have to use setAcceptHoverEvents(True) to enable events of the hover type.

    With the above the solution is:

    class CustomItem (QtWidgets.QGraphicsRectItem):
        def __init__(self, parent = None, scene = None):
            super(CustomItem, self).__init__()
            self.setAcceptHoverEvents(True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) 
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) 
            self.setRect(200,200,120,25) #Creates and show the rectangle
    
        def contextMenuEvent(self, event): #Defines the menu shown on mouse right-click
            self.scene().clearSelection()
            self.setSelected(True)
            self.RCMenu.exec_(event.screenPos()) 
    
        def mousePressEvent(self, event):
            self.setBrush(QtGui.QBrush(QtCore.Qt.cyan))
            super(CustomItem, self).mousePressEvent(event)
    
        def mouseReleaseEvent(self, event):
            self.setBrush(QtGui.QBrush(QtCore.Qt.white))
            super(CustomItem, self).mouseReleaseEvent(event)
    
        def hoverEnterEvent(self, event):
            self.setBrush(QtGui.QBrush(QtCore.Qt.lightGray))
            super(CustomItem, self).hoverEnterEvent(event)
    
        def hoverLeaveEvent(self, event):
            self.setBrush(QtGui.QBrush(QtCore.Qt.white))
            super(CustomItem, self).hoverLeaveEvent(event)
    

    On the other hand there is a problem in the code that you use to delete items but now it is not visible because until now you are only deleting element by element, but when you want to eliminate a group of elements you will see the problems, when you delete items from a list you must go to the last at the beginning because if you can not have problems accessing unallocated memory, in your case the solution is:

    def delRectangle(self):
        for item in reversed(self.scene.selectedItems()):
            self.scene.removeItem(item)