[Python] PyQt6 QGraphicsView: Zoom at mouse position resets to center with customized panning

Discussão em 'Python' iniciado por Stack, Março 15, 2025.

  1. Stack

    Stack Membro Participativo

    Problem Description


    I'm implementing a custom PyQt6 QGraphicsView with zooming and panning functionality. When I zoom in or out using the mouse wheel, the view correctly scales but then immediately resets to center on the scene instead of maintaining the position under the mouse cursor.

    Minimal Reproducible Example


    import sys
    from PyQt6 import QtCore, QtGui, QtWidgets
    import math

    class CustomGraphicsView(QtWidgets.QGraphicsView):
    def __init__(self):
    super().__init__()

    # Setup scene
    self.scene = QtWidgets.QGraphicsScene()
    self.setScene(self.scene)

    # Add some items to the scene
    for i in range(10):
    for j in range(10):
    rect = QtWidgets.QGraphicsRectItem(i*100, j*100, 50, 50)
    self.scene.addItem(rect)

    # Center point in red
    center = QtWidgets.QGraphicsEllipseItem(-10, -10, 20, 20)
    center.setBrush(QtGui.QBrush(QtGui.QColor(255, 0, 0)))
    self.scene.addItem(center)

    # Zoom settings
    self._zoom = 0
    self.MAX_ZOOM = 10
    self.MIN_ZOOM = -10

    # Set very large scene rect
    self.setSceneRect(-float("inf"), -float("inf"), float("inf"), float("inf"))

    # Configure view
    self.setTransformationAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
    self.setResizeAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
    self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
    self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
    self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag)
    self.setMouseTracking(True)

    # Panning settings
    self._ctrlLeftDrag = False
    self._lastDragPoint = QtCore.QPoint()

    # Set initial scrollbar ranges
    self.updateScrollBarRanges()

    def wheelEvent(self, event: QtGui.QWheelEvent):
    zoomInFactor = 1.25
    zoomOutFactor = 1 / zoomInFactor

    # Get the scene position before scaling
    oldPos = self.mapToScene(event.position().toPoint())

    # Determine zoom direction
    if event.angleDelta().y() > 0:
    zoomFactor = zoomInFactor
    new_zoom = self._zoom + 1
    else:
    zoomFactor = zoomOutFactor
    new_zoom = self._zoom - 1

    # Check zoom limits
    if new_zoom > self.MAX_ZOOM or new_zoom < self.MIN_ZOOM:
    return

    # Apply zoom
    self.scale(zoomFactor, zoomFactor)
    self._zoom = new_zoom

    # Get the new position under mouse after scaling
    newPos = self.mapToScene(event.position().toPoint())

    # Move scene to delta to keep mouse position fixed
    delta = newPos - oldPos
    self.translate(delta.x(), delta.y())

    # Adjust scrollbar ranges based on zoom level
    self.updateScrollBarRanges()

    def updateScrollBarRanges(self):
    # Get current transform scale
    scale = self.transform().m11()

    # Base range that stays within 32-bit integer limits
    MAX_RANGE = 2000000000

    # Calculate new range based on scale
    adjusted_range = int(min(MAX_RANGE * (1 / scale), MAX_RANGE))

    # Set new ranges for both scrollbars
    self.horizontalScrollBar().setRange(-adjusted_range, adjusted_range)
    self.verticalScrollBar().setRange(-adjusted_range, adjusted_range)

    def mousePressEvent(self, event: QtGui.QMouseEvent):
    # Check for Ctrl + Left Button for panning
    if (event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier and
    event.button() == QtCore.Qt.MouseButton.LeftButton):
    self._ctrlLeftDrag = True
    self._lastDragPoint = event.pos()
    self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ClosedHandCursor))
    event.accept()
    return
    # Middle mouse button for panning
    elif event.button() == QtCore.Qt.MouseButton.MiddleButton:
    self._ctrlLeftDrag = True
    self._lastDragPoint = event.pos()
    self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ClosedHandCursor))
    event.accept()
    return
    super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
    if self._ctrlLeftDrag:
    delta = event.pos() - self._lastDragPoint
    self._lastDragPoint = event.pos()

    # Use horizontal and vertical scrollbars for smooth scrolling
    self.horizontalScrollBar().setValue(
    self.horizontalScrollBar().value() - delta.x()
    )
    self.verticalScrollBar().setValue(
    self.verticalScrollBar().value() - delta.y()
    )
    event.accept()
    return
    super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent):
    if ((event.button() == QtCore.Qt.MouseButton.LeftButton or
    event.button() == QtCore.Qt.MouseButton.MiddleButton) and
    self._ctrlLeftDrag):
    self._ctrlLeftDrag = False
    self.viewport().unsetCursor()
    event.accept()
    return
    super().mouseReleaseEvent(event)

    def keyPressEvent(self, event: QtGui.QKeyEvent):
    if event.key() == QtCore.Qt.Key.Key_Control:
    self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.OpenHandCursor))
    super().keyPressEvent(event)

    def keyReleaseEvent(self, event: QtGui.QKeyEvent):
    if event.key() == QtCore.Qt.Key.Key_Control:
    self.viewport().unsetCursor()
    super().keyReleaseEvent(event)


    if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    view = CustomGraphicsView()
    view.show()
    view.resize(800, 600)
    sys.exit(app.exec())

    The Issue


    When running this code:

    1. Use the mouse wheel to zoom in or out
    2. The view initially zooms correctly at the mouse position
    3. But then immediately resets/centers the view, losing the mouse position as the zoom anchor
    What I've Tried


    I've attempted several approaches:

    • Disabling transformation anchors and manually applying translations
    • Blocking scrollbar signals during range updates
    • Saving view center points and restoring them
    • Setting custom transformation anchors (currently using AnchorUnderMouse)

    None of these solutions worked - the view still resets to the center of the scene after zooming, while correctly applying the zoom level.

    Important Requirements

    1. The zoom center must stay under the mouse cursor position
    2. Ctrl+Left-click and Middle-click panning must continue to work
    3. The scrollbar ranges need to be updated during zoom for large scenes
    4. The view should not freeze when panning after zooming
    Question


    How can I implement a QGraphicsView zoom function that maintains the cursor position as the zoom anchor point without resetting to the center of the scene, while correctly updating scrollbar ranges and preserving my custom panning functionality?

    Environment

    • PyQt6 6.5.0
    • Python 3.10

    ** I noticed the problem happens when the scrollbars do not have any scroll space **

    Continue reading...

Compartilhe esta Página