2

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Vlad
  • 387
  • 3
  • 17

2 Answers2

4

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.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks again :) I didn't know that event.pos() gives only positive value. I thought it's pointing always where your mouse is on x,y coordinates. Also was a nice discovery for me, use of self.transform(). But was is it necessary ? You wrote in a comments that it's for scaling ratios , but you can get from sceneRect width and height , right? Is it not same? – Vlad Dec 09 '19 at 09:24
  • 1
    They are xy coordinates, but they are positive starting from the top left corner down to the bottom right (of course, if you drag the mouse outside the window and beyond the top left they'll be negative, but that's not the point). Using transform() is necessary as the view's sceneRect might be smaller or bigger than the viewport rect (the visible portion of the scene within the scroll area boundaries) and it could be different from the actual scene's sceneRect, but this will tell you nothing about scaling: if the scene rect is (0, 0, 100, 100) and you zoom in, the rect will still be the same. – musicamante Dec 09 '19 at 13:49
0

I found this method to be easier, this uses middle mouse button to move the scene

class EditorView(QGraphicsView):
    def __init__(self, parent=None):
        super(EditorView, self).__init__(parent)

    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.MiddleButton:
            self.middleMouseButtonPressEvent(event)
        else:
            super(EditorView, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        if event.button() == Qt.MiddleButton:
            self.middleMouseButtonReleaseEvent(event)
        else:
            super(EditorView, self).mouseReleaseEvent(event)

    def middleMouseButtonPressEvent(self, event) -> None:
        self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
        fake_event = QMouseEvent(event.type(), event.localPos(), Qt.LeftButton, event.buttons(), event.modifiers())
        super(EditorView, self).mousePressEvent(fake_event)

    def middleMouseButtonReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        self.setDragMode(QGraphicsView.DragMode.NoDrag)
        super(EditorView, self).mouseReleaseEvent(event)
sandeep p
  • 21
  • 7