Search code examples
pythonpyqtpyqt5qgraphicsviewqgraphicsscene

How to translate/drag/move QGraphicsScene?


I know this question was answered a lot of times especially in C++ version of Qt, but I am not so good in C++ and I can't find the solution.

I have a code with QGraphicsView with rectangle made out of QGraphicsPolygonItem in center. I am trying to find a way to make QGraphicsScene translatable/movable/dragable by a user(anything would be okay, I am just trying to give a user an option to move around scene). But none of my tries would work.

I tried setting :

  • self.horizontalScrollBar().setValue() and self.verticalScrollBar().setValue()

  • self._scene.setSceneRect(x,y,w,h)

  • setting anchor to AnchorUnderMouse and NoAnchor

  • using translate()

None of it makes my scene move... Only thing which made my scene move is setSceneRect(), but once I put it under mouseMoveEvent(self,event) it stops working. Can somebody help me to learn how to move around that rectangle in scene ?

Code:

from PyQt5.QtGui import QColor, QPolygonF, QPen, QBrush
from PyQt5.QtCore import Qt, QPointF, QPoint, pyqtSignal
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsPolygonItem, QApplication, \
    QFrame, QSizePolicy

points_list = [[60.1, 19.6, 0.0], [60.1, 6.5, 0.0], [60.1, -6.5, 0.0], [60.1, -19.6, 0.0], [60.1, -19.6, 0.0],
               [20.0, -19.6, 0.0], [-20, -19.6, 0.0], [-60.1, -19.6, 0.0], [-60.1, -19.6, 0.0], [-60.1, -6.5, 0.0],
               [-60.1, 6.5, 0.0], [-60.1, 19.6, 0.0], [-60.1, 19.6, 0.0], [-20.0, 19.6, 0.0], [20.0, 19.6, 0.0],
               [60.1, 19.6, 0.0]]


class MainWindow(QDialog):
    def __init__(self, parent=None):
        QDialog.__init__(self, parent=parent)
        self.create()

    def create(self, **kwargs):
        main_layout = QVBoxLayout()
        graphics = MainGraphicsWidget()
        main_layout.addWidget(graphics)
        self.setLayout(main_layout)

class MainGraphicsWidget(QGraphicsView):
    zoom_signal = pyqtSignal(bool)

    def __init__(self, parent=None):
        super(MainGraphicsWidget, self).__init__(parent)
        self._scene = QGraphicsScene(backgroundBrush=Qt.gray)
        self.__zoom = 0
        self.setScene(self._scene)
        self.setTransformationAnchor(QGraphicsView.NoAnchor)
        #self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setBackgroundBrush(QBrush(QColor(30, 30, 30)))
        self.setFrameShape(QFrame.NoFrame)
        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.sceneRect = self._scene.sceneRect()
        self.testButton = GraphicsButton()
        self._scene.addItem(self.testButton)
        #self.horizontalScrollBar().setValue(199)
        #self.verticalScrollBar().setValue(500)

    def mouseMoveEvent(self, event):
        modifierPressed = QApplication.keyboardModifiers()
        if (modifierPressed & Qt.AltModifier) == Qt.AltModifier and event.buttons() == Qt.LeftButton:
            #self._scene.setSceneRect(event.pos().x(), event.pos().y(), self.sceneRect.width(), self.sceneRect.height())
            pass

        super(MainGraphicsWidget, self).mouseMoveEvent(event)

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            factor = 1.25
            self.__zoom += 1
        else:
            factor = 0.8
            self.__zoom -= 1
        self.scale(factor, factor)
        self.zoom_signal.emit(self.__zoom < 10)


class GraphicsButton(QGraphicsPolygonItem):
    def __init__(self, parent=None):
        super(GraphicsButton, self).__init__(parent)
        self.myPolygon = QPolygonF([QPointF(v1, v2) for v1, v2, v3 in points_list])
        self.setPen(QPen(QColor(0, 0, 0), 0, Qt.SolidLine, Qt.FlatCap, Qt.MiterJoin))
        self.setPolygon(self.myPolygon)
        self.setBrush(QColor(220, 40, 30))


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = MainWindow()
    window.setGeometry(500, 100, 500, 900)
    window.show()
    sys.exit(app.exec_())

Solution

  • Your approach doesn't work because you are constantly "translating" the origin point of the sceneRect with the event.pos() coordinates, and since those values are always positive, you are "focusing" on a rectangle that is apparently translated in the opposite direction.
    For example, if you drag moving your mouse on your right, it's like moving a camera on the right: the contents of the picture will be "moved" on the left.

    While using negative x and y positions would be the most logic solution, it wouldn't be effective for a real "drag" operation, since the coordinates are based on the widget; if you drag far from the origin point (the top left corner of the widget), the translation will be bigger: if you start dragging from the center of the view, the scene rectangle will be translated by 250 pixel horizontally and 450 vertically (since your window size is 500x900).

    The best approach is to keep track of the previous mouse position (starting from the mouse press event) and translate the scene rect by the difference between the mouseMoveEvent position.
    Since there could be some scaling applied to the scene (as you are using the wheel to zoom), we have to take into account that ratios too.

    class MainGraphicsWidget(QGraphicsView):
        zoom_signal = pyqtSignal(bool)
    
        def __init__(self, parent=None):
            super(MainGraphicsWidget, self).__init__(parent)
            # ...
            # I'm commenting this line, as sceneRect is a property of QGraphicsView
            # and should not be overwritten
            # self.sceneRect = self._scene.sceneRect()
            self.testButton = GraphicsButton()
            self._scene.addItem(self.testButton)
            self.startPos = None
    
    
        def mousePressEvent(self, event):
            if event.modifiers() & Qt.ControlModifier and event.button() == Qt.LeftButton:
                # store the origin point
                self.startPos = event.pos()
            else:
                super(MainGraphicsWidget, self).mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self.startPos is not None:
                # compute the difference between the current cursor position and the
                # previous saved origin point
                delta = self.startPos - event.pos()
                # get the current transformation (which is a matrix that includes the
                # scaling ratios
                transform = self.transform()
                # m11 refers to the horizontal scale, m22 to the vertical scale;
                # divide the delta by their corresponding ratio
                deltaX = delta.x() / transform.m11()
                deltaY = delta.y() / transform.m22()
                # translate the current sceneRect by the delta
                self.setSceneRect(self.sceneRect().translated(deltaX, deltaY))
                # update the new origin point to the current position
                self.startPos = event.pos()
            else:
                super(MainGraphicsWidget, self).mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            self.startPos = None
            super(MainGraphicsWidget, self).mouseReleaseEvent(event)
    

    Note that I used ControlModifier because on Linux the Alt modifier is commonly used to move windows.