In a document management program that I’m writing I’m displaying images in a QGraphicsView. The QGraphicsView supports a drag mode that allows a user to simply click and drag within the view to pan around, which works great when a large image is displayed and the user has zoomed in quite far.

Panning the image this way does mean that the user has to click back on the image and move the mouse again when the mouse reaches the end of the screen. I needed a way to implement mouse wrapping so that when the mouse reaches the end of the QGraphicsView during panning and the user is still holding down the left mouse button the mouse will jump to the opposite edge, giving a smooth continuous pan.

Here’s what I came up with…

from PyQt4.QtCore import Qt, QEvent, QTimer
from PyQt4.QtGui import QWidget, QGraphicsView, QVBoxLayout, \
    QApplication, QGraphicsScene, QBrush, QColor, QMouseEvent, \
    QCursor


class GraphicsView(QGraphicsView):

    def mouseMoveEvent(self, event):
        width, height = self.width(), self.height()
        event_x, event_y = event.x(), event.y()

        if event_y < 0 or event_y > height or \
                event_x < 0 or event_x > width:
            # Mouse cursor has left the widget. Wrap the mouse.
            global_pos = self.mapToGlobal(event.pos())
            if event_y < 0 or event_y > height:
                # Cursor left on the y axis. Move cursor to the
                # opposite side.
                global_pos.setY(global_pos.y() +
                                (height if event_y < 0 else -height))
            else:
                # Cursor left on the x axis. Move cursor to the
                # opposite side.
                global_pos.setX(global_pos.x() +
                                (width if event_x < 0 else -width))

            # For the scroll hand dragging to work with mouse wrapping
            # we have to emulate a mouse release, move the cursor and
            # then emulate a mouse press. Not doing this causes the
            # scroll hand drag to stop after the cursor has moved.
            r_event = QMouseEvent(QEvent.MouseButtonRelease,
                                    self.mapFromGlobal(QCursor.pos()),
                                    Qt.LeftButton,
                                    Qt.NoButton,
                                    Qt.NoModifier)
            self.mouseReleaseEvent(r_event)
            QCursor.setPos(global_pos)
            p_event = QMouseEvent(QEvent.MouseButtonPress,
                                  self.mapFromGlobal(QCursor.pos()),
                                  Qt.LeftButton,
                                  Qt.LeftButton,
                                  Qt.NoModifier)
            QTimer.singleShot(0, lambda: self.mousePressEvent(p_event))
        else:
            QGraphicsView.mouseMoveEvent(self, event)


class Widget(QWidget):

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)

        self.scene = QGraphicsScene()
        self.scene.setSceneRect(-5000, -5000, 5000, 5000)

        self.g_view = GraphicsView(self.scene)
        self.g_view.setBackgroundBrush(QBrush(QColor('black'),
                                              Qt.DiagCrossPattern))
        self.g_view.setDragMode(QGraphicsView.ScrollHandDrag)
        self.g_view.scale(5, 5)

        v_layout = QVBoxLayout()
        v_layout.addWidget(self.g_view)
        self.setLayout(v_layout)


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec_())

The code above uses a sub-classed QGraphicsView. The mousePressEvent checks to see if the mouse cursor has left the QGraphicsView; if it has then it moves the mouse to the opposite edge. The bit that had me stuck were the mouseReleaseEvent and mousePressEvent calls. When moving the cursor the drag was still active, so the image just panned back as if the user had moved the mouse.

The mouse events are called to simulate releasing the mouse button, moving the pointer and then pressing the mouse button again. This gives a smooth continuous pan until you run out of either image or desk space :).


Comments

comments powered by Disqus