4
votes

I'm attempting to create a QGraphicsView with the following behaviour:

  • When the control key is held and the left mouse is down, the view should be set to ScrollHandDrag mode to allow the user to pan around.

  • When in ScrollHandDrag mode, items should not be selectable/movable, as in the question here: In ScrollHandDrag mode of QGraphicsView, How to stop movement of QGraphicsItems on scene?

  • If the control key was held, the left mouse was clicked, and then the control key was released, then the view should stay in ScrollHandDrag mode until the mouse is released, or will stay in this mode should the control key be down at the time the mouse is released.

To me this sounds like it should be fairly straightforward. I've implemented the logic from the linked question, and some additional logic for my extra requirements. However this seems to cause the following two showstoppers:

  • In the mousePressEvent, setting the item under the mouse to not have the movable and selectable flags, calling the base class, and then re-applying the flags causes the item to become "frozen". The only way to solve this seems to be a control + click, release control + release click a few times outside of the item. Also when it gets into this state no items can be moved (although they can still be selected).

  • Double clicking the view causes a mousePressEvent, which is then followed by two mouseReleaseEvents! This breaks my logic.

So I would like to know how I can solve the issue of the items becoming frozen when the logic from In ScrollHandDrag mode of QGraphicsView, How to stop movement of QGraphicsItems on scene? is used, and how to deal with the strange double click mouse events - is there a way to turn them off?

Here is my code (this is also pretty much my hello world Python, so if I've made some horrible Python mistake please let me know):

#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *

class MyMainWindow(QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("Test")
        self.gv = MyGraphicsView()
        self.setCentralWidget(self.gv)
        self.setGeometry(170, 130, 450, 250)

class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setup()

    def setup(self):
        self.m_MouseIsDown = False
        self.m_ControlKeyDown = False
        self.setDragMode(QGraphicsView.RubberBandDrag)

    def mouseMoveEvent(self,  event):
       # print "mouseMoveEvent: " + str(event.pos().x()) + "," + str(event.pos().y())
        super(MyGraphicsView,  self).mouseMoveEvent(event);

    def mousePressEvent(self,  event):
        print "mousePressEvent"

        itemUnderMouse = self.itemAt(event.pos())
        if  itemUnderMouse is not None:
            bHadMovableFlagSet = itemUnderMouse.flags() & QGraphicsItem.ItemIsMovable
            bWasSelected = itemUnderMouse.isSelected()            
            bHadSelectableFlagSet = itemUnderMouse.flags() & QGraphicsItem.ItemIsSelectable
            if bHadMovableFlagSet:
                print "has ItemIsMovable"
            else:
                print "hasn't ItemIsMovable"
            if bHadSelectableFlagSet:
                print "has ItemIsSelectable"
            else:
                print "hasn't ItemIsSelectable"
            if bWasSelected:
                print "isSelected true"
            else:
                print "isSelected false"
            itemUnderMouse.setSelected(False)

        if event.button() == Qt.LeftButton:
            print "mousePressEvent: left button is now down"
            self.m_MouseIsDown = True

        if self.dragMode() == QGraphicsView.ScrollHandDrag and event.button() == Qt.LeftButton:
            print "mousePressEvent: left button down and ScrollHandDrag set"
            self.PreventItemsFromMovingOrBeingSelectedWhenPannning(event)
            return

        print "mousePressEvent: pass through"
        super(MyGraphicsView,  self).mousePressEvent(event)

    def mouseReleaseEvent(self,  event):
        print "mouseReleaseEvent"
        if event.button() == Qt.LeftButton:
            print "mouseReleaseEvent - left button is now up"
            self.m_MouseIsDown = False
            if self.dragMode() == QGraphicsView.ScrollHandDrag and self.m_ControlKeyDown == False:
                print "mouseReleaseEvent - left button up, in ScrollHandDrag mode and control key is not pressed, change to RubberBandDrag"
                self.setDragMode(QGraphicsView.RubberBandDrag)

        super(MyGraphicsView,  self).mouseReleaseEvent(event)

    def keyPressEvent(self,  event):
        if event.key() == Qt.Key_Control:
            print "control key down"
            self.m_ControlKeyDown = True

        # ignore if mouse already down since we don't want to suddenly change to pan mode if an item is being moved
        if event.key() == Qt.Key_Control and self.dragMode() != QGraphicsView.ScrollHandDrag and self.m_MouseIsDown == False:
            print "keyPressEvent - control key down, mouse isn't down and drag mode is not ScrollHandDrag, change to ScrollHandDrag"
            self.setDragMode(QGraphicsView.ScrollHandDrag)
        super(MyGraphicsView,  self).keyPressEvent(event)

    def keyReleaseEvent(self,  event):
        if event.key() == Qt.Key_Control:
            print "control key up"
            self.m_ControlKeyDown = False

        if event.key() == Qt.Key_Control and self.dragMode() == QGraphicsView.ScrollHandDrag and self.m_MouseIsDown == False:
            print "keyReleaseEvent - control key up and drag mode is ScrollHandDrag, mouse is not pressed, change to RubberBandDrag"
            self.setDragMode(QGraphicsView.RubberBandDrag)
        super(MyGraphicsView,  self).keyReleaseEvent(event)

    def wheelEvent(self,  event):
        factor = 1.2;
        if event.delta() < 0:
            factor = 1.0 / factor
        self.scale(factor, factor)

    def PreventItemsFromMovingOrBeingSelectedWhenPannning(self,  mouseEvent):
        itemUnderMouse = self.itemAt(mouseEvent.pos())
        if  itemUnderMouse is not None:
            print "preventing item from moving"
            bHadMovableFlagSet = itemUnderMouse.flags() & QGraphicsItem.ItemIsMovable
            itemUnderMouse.setFlag(QGraphicsItem.ItemIsMovable,  False)

            bWasSelected = itemUnderMouse.isSelected()

            bHadSelectableFlagSet = itemUnderMouse.flags() & QGraphicsItem.ItemIsSelectable
            itemUnderMouse.setFlag(QGraphicsItem.ItemIsSelectable,  False)

            super(MyGraphicsView,  self).mousePressEvent(mouseEvent)

            if bHadMovableFlagSet:
                print "set ItemIsMovable"
                itemUnderMouse.setFlag(QGraphicsItem.ItemIsMovable,  True)
            if bHadSelectableFlagSet:
                print "set ItemIsSelectable"
                itemUnderMouse.setFlag(QGraphicsItem.ItemIsSelectable,  True)
            if bWasSelected:
                print "setSelected True"
                itemUnderMouse.setSelected(True)

        else:
             print "no item under mouse - pass through"
             super(MyGraphicsView,  self).mousePressEvent(mouseEvent)

class MyGraphicsScene(QGraphicsScene):
    def __init__(self,  parent):
        super(MyGraphicsScene,  self).__init__()

def main():
    a = QApplication(sys.argv)
    w = MyMainWindow()
    w.show()

    scene = MyGraphicsScene(w)
    w.gv.setScene(scene)

    rect = scene.addRect( 10,  10,  40,  40)
    rect.setFlag( QGraphicsItem.ItemIsSelectable )
    rect.setFlag( QGraphicsItem.ItemIsMovable )

    rect = scene.addRect( 40,  40,  40,  40)
    rect.setFlag( QGraphicsItem.ItemIsSelectable )
    rect.setFlag( QGraphicsItem.ItemIsMovable )

    sys.exit(a.exec_()) 

if __name__ == '__main__':
    main()
1

1 Answers

5
votes

If you don't call the base implementation in mouse*Events while panning, item selection won't be an issue. However, this now requires re-implementing the built-in panning function. Fortunately, it's not hard to implement it.

After some iteration at IRC (#pyqt @ freenode), this is the final implementation:

  • Pressing and holding CTRL key enables panning. While mouse is pressed, releasing CTRL key keeps panning mode.
  • Just mouse press activates Rubber selection
  • All actions are controlled with Left-Button
class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setDragMode(QGraphicsView.RubberBandDrag)
        self._isPanning = False
        self._mousePressed = False

    def mousePressEvent(self,  event):
        if event.button() == Qt.LeftButton:
            self._mousePressed = True
            if self._isPanning:
                self.setCursor(Qt.ClosedHandCursor)
                self._dragPos = event.pos()
                event.accept()
            else:
                super(MyGraphicsView, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self._mousePressed and self._isPanning:
            newPos = event.pos()
            diff = newPos - self._dragPos
            self._dragPos = newPos
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - diff.x())
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - diff.y())
            event.accept()
        else:
            super(MyGraphicsView, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if event.modifiers() & Qt.ControlModifier:
                self.setCursor(Qt.OpenHandCursor)
            else:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
            self._mousePressed = False
        super(MyGraphicsView, self).mouseReleaseEvent(event)

    def mouseDoubleClickEvent(self, event): pass

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Control and not self._mousePressed:
            self._isPanning = True
            self.setCursor(Qt.OpenHandCursor)
        else:
            super(MyGraphicsView, self).keyPressEvent(event)

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Control:
            if not self._mousePressed:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
        else:
            super(MyGraphicsView, self).keyPressEvent(event)


    def wheelEvent(self,  event):
        factor = 1.2;
        if event.delta() < 0:
            factor = 1.0 / factor
        self.scale(factor, factor)