Search code examples
qtpyqt5qgraphicsitemqmouseevent

PyQt5: QGraphicsScene: Mouse item with click and drop (without holding press)


The default mouseEvent function in QGraphicsScene allows to move the Item by press-and-hold, move, and release. I am trying to overwrite the QGraphicsScene mouseEvent() function to accomplish the same movement with press-and-release-once (pick item), move (without holding press), press-and-release-twice (drop item). I though it would be as simple as:

self.moving = False
def mousePressEvent(self, event):
    if event.button() == Qt.LeftButton:
        if self.moving == False: # first click, pick up, start moving
            super().mousePressEvent(event)
            self.moving = True
        else:                    # second click, drop, end moving item
            super().mouseReleaseEvent(event)
            self.moving = False    

def mouseMoveEvent(self, event):      
    if self.moving == True:
        super().mousePressEvent(event)
        super().mouseMoveEvent(event)

def mouseReleaseEvent(self, event):      
    pass

I am not able to pick up and move the item so far, does anyone spot anything wrong? Also, where can I find the original implementation of the QGraphicsScene mouseEvent function?

Thank you!


Solution

  • First of all, you shall never call the base implementation of an event handler with a wrong argument type.
    Calling mousePressEvent using a mouse move event as its argument is conceptually wrong, and while it's generally not "dangerous" when done on graphics items, it usually causes a fatal crash when done on widgets.

    That said, if you want to follow the mouse upon mouse press and release, that obviously means that you have to change the "follow state" within the mouseReleaseEvent() handler, not the mousePressEvent() one.

    Furthermore, you must ensure that:

    1. the item becomes the mouse grabber after the mouse button has been released;;
    2. the item also accepts hover events, so that any hover event (most importantly, hover move events) are always received and handled by the item;

    About the first point, generally speaking a widget (or graphics item) becomes the mouse grabber when any mouse button is pressed on it and until that button has been released. Becoming a mouse grabber means that all mouse events are dispatched to that item until it releases the mouse; since you want to be able to receive mouse events after releasing the mouse button, you have to explicitly grab (or ungrab) the mouse when the button is released.

    In the following example I'm showing how to implement the above, and also providing support for items that are already movable (through the ItemIsMovable flag). Items that show the "Movable flag" text can also be moved while keeping the left button pressed, otherwise only the press/release support is provided for other items.

    Be aware that moving items based on mouse events has to consider the item transformations: the pos() of events is always mapped in local coordinates, so if you click on the top left corner of a rectangle and the item is rotated, you will always get that top left corner position. Since setPos() uses the parent coordinate system, we have to map those positions to the parent in order to achieve proper movement. This also means that complex transformations (considering the parent) could make things much more difficult, so be aware of that. For more complex scenarios, you might have to further implement the computation of the target position, or stick with the default behavior.

    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    
    class NoMouseButtonMoveRectItem(QGraphicsRectItem):
        moving = False
        def mousePressEvent(self, event):
            super().mousePressEvent(event)
            if event.button() == Qt.LeftButton:
                # by defaults, mouse press events are not accepted/handled,
                # meaning that no further mouseMoveEvent or mouseReleaseEvent
                # will *ever* be received by this item; with the following,
                # those events will be properly dispatched
                event.accept()
                self.pressPos = event.screenPos()
    
        def mouseMoveEvent(self, event):
            if self.moving:
                # map the position to the parent in order to ensure that the
                # transformations are properly considered:
                currentParentPos = self.mapToParent(
                    self.mapFromScene(event.scenePos()))
                originParentPos = self.mapToParent(
                    self.mapFromScene(event.buttonDownScenePos(Qt.LeftButton)))
                self.setPos(self.startPos + currentParentPos - originParentPos)
            else:
                super().mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            super().mouseReleaseEvent(event)
            if (event.button() != Qt.LeftButton 
                or event.pos() not in self.boundingRect()):
                    return
    
            # the following code block is to allow compatibility with the
            # ItemIsMovable flag: if the item has the flag set and was moved while
            # keeping the left mouse button pressed, we proceed with our
            # no-mouse-button-moved approach only *if* the difference between the
            # pressed and released mouse positions is smaller than the application
            # default value for drag movements; in this way, small, involuntary
            # movements usually created between pressing and releasing the mouse
            # button will still be considered as candidates for our implementation;
            # if you are *not* interested in this flag, just ignore this code block
            distance = (event.screenPos() - self.pressPos).manhattanLength()
            if (not self.moving and distance > QApplication.startDragDistance()):
                return
            # end of ItemIsMovable support
    
            self.moving = not self.moving
            # the following is *mandatory*
            self.setAcceptHoverEvents(self.moving)
            if self.moving:
                self.startPos = self.pos()
                self.grabMouse()
            else:
                self.ungrabMouse()
    
    
    if __name__ == '__main__':
        import sys
        from random import randrange, choice
        app = QApplication(sys.argv)
        scene = QGraphicsScene()
        view = QGraphicsView(scene)
        view.resize(QApplication.primaryScreen().size() * 2 / 3)
        # create random items that support click/release motion
        for i in range(10):
            item = NoMouseButtonMoveRectItem(0, 0, 100, 100)
            item.setPos(randrange(500), randrange(500))
            item.setPen(QColor(*(randrange(255) for _ in range(3))))
            if choice((0, 1)):
                item.setFlags(item.ItemIsMovable)
                QGraphicsSimpleTextItem('Movable flag', item)
            else:
                item.setBrush(QColor(*(randrange(255) for _ in range(3))))
            scene.addItem(item)
        view.show()
        sys.exit(app.exec_())