0

The given code below is derived from another question on SO.

I have changed the way the mouse movements are used to calculate values for the progress bars and added a reset button. I noticed that mouse wheel rotations with approximately the same speed and angle are not always evaluated in the same way. Sometimes the progress bar "jumps" to almost 100% when the mouse wheel is moved quickly, and in other cases at almost the same speed and angle it does not even reach 50%.

What is the reason for this different behaviour (and how to fix it)?


main.py

import sys
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QPoint, QEvent
from PyQt5.QtGui import QCursor, QPainterPath, QPen
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPathItem, QWidget
from PyQt5.uic import loadUi

app = None


class MouseListener(QObject):
    posChanged = pyqtSignal(QPoint)
    wheelChanged = pyqtSignal(QPoint)

    def __init__(self, widget):
        super().__init__(widget)
        self._widget = widget
        self._childrens = []

        self._setup_widget(self._widget)

        for w in self._widget.findChildren(QWidget):
            self._setup_widget(w)
            self._childrens.append(w)

    def _setup_widget(self, w):
        w.installEventFilter(self)
        w.setMouseTracking(True)

    def eventFilter(self, obj, event):
        if obj in [self._widget] + self._childrens and event.type() == QEvent.MouseMove:
            self.posChanged.emit(event.globalPos())

        if event.type() == QEvent.Wheel:
            self.wheelChanged.emit(event.angleDelta())

        if event.type() == QEvent.ChildAdded:
            obj = event.child()
            if obj.isWidgetType():
                self._setup_widget(obj)
                self._childrens.append(obj)

        if event.type() == QEvent.ChildRemoved:
            c = event.child()
            if c in self._childrens:
                c.removeEventFilter(self)
                self._childrens.remove(c)
        return super().eventFilter(obj, event)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        loadUi("mainwindow.ui", self)
        self.showMaximized()
        self.global_pos = QCursor.pos()

        self.reset_button.clicked.connect(self._reset_progress_bars)

        for lay in (self.verticalLayout_top, self.verticalLayout_bottom):
            view = GraphicsView()
            lay.addWidget(view)

        window_listener = MouseListener(self)
        window_listener.posChanged.connect(self.on_pos_changed)
        window_listener.wheelChanged.connect(self.on_wheel_changed)

    @staticmethod
    def _update_bar(progress_bar, first, second):
        delta = abs(first - second)
        current_value = progress_bar.value()
        new_value = current_value + delta
        progress_bar.setValue(new_value)

    def _reset_progress_bars(self):
        self.progressBar_x_plus.setValue(0)
        self.progressBar_x_minus.setValue(0)
        self.progressBar_y_plus.setValue(0)
        self.progressBar_y_minus.setValue(0)
        self.progressBar_w_plus.setValue(0)
        self.progressBar_w_minus.setValue(0)

    def on_pos_changed(self, pos):
        new_x = pos.x()
        new_y = pos.y()
        old_x = self.global_pos.x()
        old_y = self.global_pos.y()
        if new_x > old_x:
            self._update_bar(self.progressBar_x_plus, old_x, new_x)
        if new_x < old_x:
            self._update_bar(self.progressBar_x_minus, old_x, new_x)
        if new_y > old_y:
            self._update_bar(self.progressBar_y_plus, old_y, new_y)
        if new_y < old_y:
            self._update_bar(self.progressBar_y_minus, old_y, new_y)
        self.global_pos = pos

    def on_wheel_changed(self, pos):
        new_w = pos.y()
        if new_w > 0:
            self._update_bar(self.progressBar_w_plus, 0, new_w)
            print("W+", new_w)
        if new_w < 0:
            self._update_bar(self.progressBar_w_minus, 0, new_w)
            print("W-", new_w)


class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.start = None
        self.end = None

        self.setScene(QGraphicsScene())
        self.path = QPainterPath()
        self.item = GraphicsPathItem()
        self.scene().addItem(self.item)

        self.contents_rect = self.contentsRect()
        self.setSceneRect(0, 0, self.contents_rect.width(), self.contents_rect.height())
        self.horizontalScrollBar().blockSignals(True)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.verticalScrollBar().blockSignals(True)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def mousePressEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.start = self.mapToScene(event.pos())
            self.path.moveTo(self.start)
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.end = self.mapToScene(event.pos())
            self.path.lineTo(self.end)
            self.start = self.end
            self.item.setPath(self.path)
        super().mouseMoveEvent(event)


class GraphicsPathItem(QGraphicsPathItem):
    def __init__(self):
        super().__init__()
        pen = QPen()
        pen.setColor(Qt.black)
        pen.setWidth(5)
        self.setPen(pen)


def main():
    global app
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

mainwindow.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>1003</width>
    <height>703</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Mouse Pointer</string>
  </property>
  <property name="locale">
   <locale language="English" country="UnitedKingdom"/>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout_2">
    <item row="3" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_bottom"/>
    </item>
    <item row="0" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_top"/>
    </item>
    <item row="1" column="0">
     <layout class="QGridLayout" name="gridLayout">
      <item row="2" column="1">
       <widget class="QProgressBar" name="progressBar_y_plus">
        <property name="maximum">
         <number>1000</number>
        </property>
        <property name="value">
         <number>0</number>
        </property>
       </widget>
      </item>
      <item row="1" column="1">
       <widget class="QProgressBar" name="progressBar_x_minus">
        <property name="maximum">
         <number>1000</number>
        </property>
        <property name="value">
         <number>0</number>
        </property>
       </widget>
      </item>
      <item row="0" column="1">
       <widget class="QProgressBar" name="progressBar_x_plus">
        <property name="maximum">
         <number>1000</number>
        </property>
        <property name="value">
         <number>0</number>
        </property>
       </widget>
      </item>
      <item row="3" column="0">
       <widget class="QLabel" name="label_y_minus">
        <property name="text">
         <string>Y-</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="0" column="0">
       <widget class="QLabel" name="label_x_plus">
        <property name="text">
         <string>X+</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="3" column="1">
       <widget class="QProgressBar" name="progressBar_y_minus">
        <property name="maximum">
         <number>1000</number>
        </property>
        <property name="value">
         <number>0</number>
        </property>
       </widget>
      </item>
      <item row="2" column="0">
       <widget class="QLabel" name="label_y_plus">
        <property name="text">
         <string>Y+</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="label_x_minus">
        <property name="text">
         <string>X-</string>
        </property>
        <property name="alignment">
         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
        </property>
       </widget>
      </item>
      <item row="4" column="1">
       <widget class="QProgressBar" name="progressBar_w_plus">
        <property name="maximum">
         <number>10000</number>
        </property>
        <property name="value">
         <number>0</number>
        </property>
       </widget>
      </item>
      <item row="5" column="1">
       <widget class="QProgressBar" name="progressBar_w_minus">
        <property name="maximum">
         <number>10000</number>
        </property>
        <property name="value">
         <number>0</number>
        </property>
       </widget>
      </item>
      <item row="4" column="0">
       <widget class="QLabel" name="label_w_plus">
        <property name="text">
         <string>W+</string>
        </property>
       </widget>
      </item>
      <item row="5" column="0">
       <widget class="QLabel" name="label_w_minus">
        <property name="text">
         <string>W-</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item row="2" column="0">
     <widget class="QPushButton" name="reset_button">
      <property name="text">
       <string>Reset bars</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>1003</width>
     <height>24</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>
Atalanttore
  • 349
  • 5
  • 22

1 Answers1

1

When an object receives an event, its event() function handles it.
If the object supports that event in some way, it could return False, meaning that the event will be then processed by its ancestors, if any (for widgets, it means its immediate parent) and it will go up to the top-level object until any of them will return True.
Consider that some events like QWheelEvent can be processed by a widget and still being sent "back" to their parents (by returning False), and this doesn't necessarily have something to do with setting the event as accepted or ignored.

So, what you are getting when you see an "accelerated" value is actually the same wheel event that is being processed by all the objects you added to the _childrens list.

Moreover, you are using a QGraphicsView (which internally creates a QGraphicsWheelEvent and behaves differently according to the scene contents and rect) that is a scroll area, a complex widget that has at least 4 child widgets: the viewport, the scroll contents and the 2 scroll bars.
Even if you are hiding the scrollbars and blocking their signals, they are still there and the scroll area interacts with them, and so does Qt by sending them the events.
Try leaving the scrollbars active and visible and you'll see that the amount of wheel events becomes bigger after a scrollbar reaches its maximum (for down scrolling) or minimum (for up): that's because they can't go over their current limit, and the event will therefore be "ignored" (with their event() returning True) and processed by its parents.
In this case, a wheel event will be more or less received and filtered in this order:

  • the graphics view's scroll area "widget", which will send it to...
  • the graphics scene (not filtered), which will send it to...
  • the topmost item that accepts wheel events (not filtered), otherwise back to
  • the graphics view's viewport
  • the graphics view's scroll bars
  • the graphics view's parent's...
  • the central widget
  • the main window

With your implementation it's really hard to distinguish between an event that has already been filtered or not, and you obviously cannot use the same concept you use for mouse movements as there's no reference to previous event data.

The only solution I can come up to is to subclass QApplication and override its notify() method:

class WheelNotifyApp(QApplication):
    wheelChanged = pyqtSignal(QPoint)
    def notify(self, obj, event):
        if event.type() == QEvent.Wheel and isinstance(obj, QWindow):
            self.wheelChanged.emit(event.angleDelta())
        return super().notify(obj, event)

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        # ...
        QApplication.instance().wheelChanged.connect(self.on_wheel_changed)
musicamante
  • 41,230
  • 6
  • 33
  • 58