1

I am doing an Image Viewer that allow user to browse their folder and show the first image in the folder. By scrolling the mouse wheel, user is able to zoom in and out the image. However, the image pan up and down too as the scroll feature of the mouse wheel is still there. I try scrollbaralwaysoff but it doestn work too. May I know how to disable the scroll feature while remaining my zooming feature?

from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import os
from tg_audit import Ui_MainWindow




class mainProgram (QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.browser()
 

    def browser(self):
        self.model = QFileSystemModel(self.centralwidget)
        self.model.setRootPath('')
        self.treeView.setModel(self.model)
        self.treeView.setAnimated(False)
        self.treeView.setIndentation(20)
        self.treeView.setSortingEnabled(True)
        self.treeView.clicked.connect(self.open_image)

    def open_image(self, index):
        #get path from browser
        path = self.sender().model().filePath(index)
        print(path)
        self.folder_path = r'{}'.format(path)
        self.list_of_images = os.listdir(self.folder_path)
        self.list_of_images = sorted(self.list_of_images)

        #path of the image
        input_img_raw_string = r'{}\\{}'.format(path,self.list_of_images[0])

        #load image path to graphic view
        self.scene = QGraphicsScene()
        self.scene.addItem(QGraphicsPixmapItem(QPixmap.fromImage(QImage(input_img_raw_string))))
        self.graphicsView.setScene(self.scene)
        self.graphicsView.fitInView(self.scene.sceneRect(),Qt.KeepAspectRatio)
        self.zoom = 1
        self.rotate = 0


    def wheelEvent(self, event):

        x = event.angleDelta().y() / 120
        if x > 0:
            self.zoom *= 1.05
            self.updateView()
        elif x < 0:
            self.zoom /= 1.05
            self.updateView()



    def updateView(self):

        self.graphicsView.setTransform(QTransform().scale(self.zoom, self.zoom).rotate(self.rotate))

if __name__ == '__main__':
    import sys
    from PySide2.QtWidgets import QApplication
    app = QApplication(sys.argv)
    imageViewer = mainProgram()
    imageViewer.show()
    sys.exit(app.exec_())
crazybin98
  • 29
  • 5

1 Answers1

1

When dealing with UI elements, it's important to remember that Qt relays events to the "current" widget (the currently focused element for keyboard events, or the topmost enabled widget under the mouse), then that widget will decide if that event can be handled or not, and in the latter case the event is then propagated to its parent, continuing to climb the object tree until a widget actually can handle that event. Please note that, in this context, handling an event doesn't mean that the widget actually reacts to it in some way: for various reasons, a widget could just accept the event and still do nothing with it (consider a disabled button).

The problem is that you're implementing the wheel event for the main window only, and this means two things:

  • the event will be possibly received even when scrolling on unrelated context; for instance, if you scroll on a QLineEdit, you will trigger the zooming, which could be very confusing to the users;
  • the zooming is only triggered as soon as the graphics view propagate it: if scroll bars are visible, they will "eat" (accept) the event until they reach their boundaries, and only at that point the event will be propagated to the main window and finally trigger the zooming;

The solution is to track the wheel event on the proper widget, the view; there are two possible solutions: subclassing and event filtering.

Subclassing

This is the common OOP approach, which provides better modularity and allows tidier reimplementations.

class View(QtWidgets.QGraphicsView):
    def __init__(self):
        super().__init__()
        self.zoom = 1
        self.rotate = 0

    def fitInView(self, *args, **kwargs):
        super().fitInView(*args, **kwargs)
        self.zoom = self.transform().m11()

    def wheelEvent(self, event):
        x = event.angleDelta().y() / 120
        if x > 0:
            self.zoom *= 1.05
            self.updateView()
        elif x < 0:
            self.zoom /= 1.05
            self.updateView()

    def updateView(self):
        self.setTransform(QtGui.QTransform().scale(self.zoom, self.zoom).rotate(self.rotate))

Note that I reimplemented fitInView in order to correctly set the zoom to the current zoom level set by the rescaling.

Event filtering

Qt allows installing event filters on any QObject (all widgets inherit from QWidget and QWidget inherits from QObject), which capture any event that is going to be received by those objects, and allows dealing with that event by ignoring it or doing anything you need.
QGraphicsView is a subclass of QAbstractScrollArea, so the widget that receives wheel events is actually its viewport. This means that the wheel event is first of all sent to that viewport (and, actually, the scene), then, if not accepted yet, it's sent to the scroll bars and finally the actual QGraphicsView widget. So, in order to avoid triggering the scrollbars, we cannot install the filter on the view, but on the viewport.

class mainProgram(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        # ...
        self.graphicsView.viewport().installEventFilter(self)

    def eventFilter(self, source, event):
        if source == self.graphicsView.viewport() and event.type() == event.Wheel:
            x = event.angleDelta().y() / 120
            if x > 0:
                self.zoom *= 1.05
                self.updateView()
            elif x < 0:
                self.zoom /= 1.05
                self.updateView()
            return True
        return super().eventFilter(source, event)

Some further considerations on your code:

  • self.sender() should always used with care and only when there's no other alternative; in your case it's pointless, since open_image is only connected to the view's clicked signal, so just use path = self.treeView.model().filePath(index);
  • self.list_of_images = sorted(self.list_of_images) is redundant, self.list_of_images.sort() is normally preferred;
  • the whole file path and pixmap loading is unnecessarily complicated and prone to errors: the raw prefix is unnecessary, as Qt already returns escaped paths; os.listdir raises an exception if the path is not a directory; there's no need to create a new scene, just create a scene in the __init__ and then call its clear();
  • QPixmap is fully capable of loading an image from path, using QImage is only required in specific cases.
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks sir! It works for the event filter. However, for the subclass method how should i call it at the main window? Because the graphical view is implemented inside the main window which the codes is generated by the Qt designer. – crazybin98 Apr 19 '21 at 02:17
  • @crazybin98 you need to *promote* widgets in order to use subclasses. See for example [this answer](https://stackoverflow.com/a/60311724/2001654) (in that case it's a custom widget, in yours just subclass QGraphicsView and promote *that* class accordingly in Designer). – musicamante Apr 19 '21 at 02:24
  • I can use subclass method too now! Thanks again for your info! – crazybin98 Apr 19 '21 at 03:04
  • @crazybin98 you're very welcome! Remember that if an answer solves your issue, you should mark it as accepted by clicking on the gray tickmark on its left. – musicamante Apr 19 '21 at 03:36