Search code examples
zoomingqscrollareapyside6qscrollbar

Zoom to mouse position - pyside6


Hello I am tring to implement simple image widget with zoom to mouse position. I combined the example at Zooming in/out on a mouser point ? and https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html. However the image do not scale as expected and the scale bars do not update appropriately either. Here is my code:

import sys
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
from PIL.ImageQt import ImageQt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QScrollArea


class MyScrollArea(QScrollArea):
    def __init__(self, imageWidget):
        # initialize widget
        super().__init__()
        self.setWidget(imageWidget)
        self.myImageWidget = imageWidget
        self.oldScale = 1
        self.newScale = 1

    def wheelEvent(self, event) -> None:
        if event.angleDelta().y() < 0:
            # zoom out
            self.newScale = 0.8
        else:
            # zoom in
            self.newScale = 1.25

        # compute scrollbar positions
        scrollBarPosHorizontal = self.horizontalScrollBar().value()
        scrollBarPosVertical = self.verticalScrollBar().value()
        deltaToPos = (event.position() / self.oldScale) - (self.myImageWidget.pos() / self.oldScale)
        delta = deltaToPos * self.newScale - deltaToPos * self.oldScale

        # resize image
        self.myImageWidget.resize(self.myImageWidget.size() * self.newScale)

        # set scrollbars
        self.horizontalScrollBar().setValue(scrollBarPosHorizontal+delta.x())
        self.verticalScrollBar().setValue(scrollBarPosVertical+delta.y())

        # save old scale
        self.oldScale = self.newScale

class ImageViewer(QDialog):
    def __init__(self, img):
        # initialize widget
        super().__init__()
        self.setWindowTitle('Zoom example')
        self.imageWidget = QLabel()
        self.imageWidget.installEventFilter(self)
        self.imageWidget.setAlignment(Qt.AlignCenter)
        self.pixmap = QPixmap.fromImage(img)
        self.imageWidget.setPixmap(self.pixmap)

        # create scroll area
        self.scrollArea = MyScrollArea(self.imageWidget)

        # insert to layout
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.scrollArea)
        self.setLayout(self.layout)


if __name__ == '__main__':
    # prepare app
    app = QtWidgets.QApplication(sys.argv)

    # prepare image
    image = ImageQt("test.png")

    # create viewer widget
    MyWidget = ImageViewer(image)
    MyWidget.show()

    # close app
    sys.exit(app.exec())

The image do not scale to mouse point at all. What am I doing wrong?


Solution

  • The main problem is that by default the pixmap is not resized when QLabel is resized, so setScaledContents(True) must be used.

    Note that the algorithm used for the zoom and translation doesn't work very well, as it doesn't consider the change in the range of the scroll bars correctly.

    I propose an alternate version that actually zooms on the mouse similarly to what happens in common image viewers/editors and map viewers. The trick is to map the mouse position to the label, and get the delta based on the scaled position:

    class MyScrollArea(QScrollArea):
        def __init__(self, imageWidget):
            # ...
            imageWidget.setScaledContents(True)
    
        # ...
    
        def wheelEvent(self, event) -> None:
            if event.angleDelta().y() < 0:
                # zoom out
                self.newScale = 0.8
            else:
                # zoom in
                self.newScale = 1.25
    
            widgetPos = self.myImageWidget.mapFrom(self, event.position())
    
            # resize image
            self.myImageWidget.resize(self.myImageWidget.size() * self.newScale)
    
            delta = widgetPos * self.newScale - widgetPos
            self.horizontalScrollBar().setValue(
                self.horizontalScrollBar().value() + delta.x())
            self.verticalScrollBar().setValue(
                self.verticalScrollBar().value() + delta.y())
            
            self.oldScale = self.newScale
    

    Note that QLabel is not well suited for such purposes (especially for big images and high zoom values). I strongly suggest you to consider switching to the Graphics View Framework.