2

I would like to track the mouse movement for calculation (of values for progress bars) while the mouse is used to draw in one of the two QGraphicsView. With the code below you can draw either in a QGraphicsView or get the coordinates of mouse movements over the gridLayout, but not both at once. How can this be done?


main.py

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

app = None


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        loadUi("mainwindow.ui", self)
        self.showMaximized()
        self.setMouseTracking(True)
        self.centralWidget().setAttribute(Qt.WA_MouseTracking)

        self._old_x = QCursor.pos().x()
        self._old_y = QCursor.pos().y()

        self.verticalLayout_top.addWidget(GraphicsView())
        self.verticalLayout_bottom.addWidget(GraphicsView())

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

    def mouseMoveEvent(self, event):
        new_x = event.x()
        new_y = event.y()

        if new_x > self._old_x:
            self._update_bar(self.progressBar_x_plus, new_x - self._old_x)
        if new_x < self._old_x:
            self._update_bar(self.progressBar_x_minus, self._old_x - new_x)

        if new_y > self._old_y:
            self._update_bar(self.progressBar_y_plus, new_y - self._old_y)
        if new_y < self._old_y:
            self._update_bar(self.progressBar_y_minus, self._old_y - new_y)

        self._old_x = new_x
        self._old_y = new_y


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.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def mousePressEvent(self, event):
        self.start = self.mapToScene(event.pos())
        self.path.moveTo(self.start)

    def mouseMoveEvent(self, event):
        self.end = self.mapToScene(event.pos())
        self.path.lineTo(self.end)
        self.start = self.end
        self.item.setPath(self.path)


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="0" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_top"/>
    </item>
    <item row="1" column="0">
     <layout class="QGridLayout" name="gridLayout">
      <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="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="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="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="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="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="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="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>
     </layout>
    </item>
    <item row="2" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_bottom"/>
    </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>

Update 1: Attempt to track mouse movements over the entire window:

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

        for lay in (self.verticalLayout_top, self.verticalLayout_bottom):
            view = GraphicsView()
            listener = MouseListener(view.viewport())
            listener.posChanged.connect(self.on_pos_changed)
            lay.addWidget(view)

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

Update 2

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)

    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:
        if event.type() == QEvent.MouseMove:
            self.posChanged.emit(event.globalPos())

        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()

        for lay in (self.verticalLayout_top, self.verticalLayout_bottom):
            view = GraphicsView()
            listener = MouseListener(view.viewport())
            listener.posChanged.connect(self.on_pos_changed)
            lay.addWidget(view)

        # window_listener = MouseListener(self)
        # window_listener.posChanged.connect(self.on_pos_changed)

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

    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, new_x - old_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, new_y - old_y)
        if new_y < old_y:
            self._update_bar(self.progressBar_y_minus, old_y - new_y)
        self.global_pos = pos


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.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        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()
Atalanttore
  • 349
  • 5
  • 22

1 Answers1

2

The problem is that the mousePressEvent does not necessarily propagate from the parent widget to the child widget (that behavior depends on each type of widget, for example QLabel if it propagates the mouse events), in addition to your strategy of overriding the mouseMoveEvent method is limited if you want Listen to the events of other widgets.

Considering the above, a possible solution is to use an eventFilter to listen to the events of any widget, and another improvement is to use the global position instead of the local one so that when the mouse changes from QGraphicsView it is not affected by the local coordinate system.

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

    def __init__(self, widget):
        super().__init__(widget)
        self._widget = widget
        self._widget.setMouseTracking(True)
        self._widget.installEventFilter(self)

    def eventFilter(self, obj, event):
        if obj is self._widget and event.type() == QEvent.MouseMove:
            self.posChanged.emit(event.globalPos())
        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()
        for lay in (self.verticalLayout_top, self.verticalLayout_bottom):
            view = GraphicsView()
            listener = MouseListener(view.viewport())
            listener.posChanged.connect(self.on_pos_changed)
            lay.addWidget(view)

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

    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, new_x - old_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, new_y - old_y)
        if new_y < old_y:
            self._update_bar(self.progressBar_y_minus, old_y - new_y)
        self.global_pos = pos

Update:

class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        # ...

    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)

Update:

In this case you must apply the eventFilter not only to the viewport of the QGraphicsView but to all the children of the window.

class MouseListener(QObject):
    posChanged = 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.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()
        listener = MouseListener(self)
        listener.posChanged.connect(self.on_pos_changed)

        for lay in (self.verticalLayout_top, self.verticalLayout_bottom):
            view = GraphicsView()
            lay.addWidget(view)
        # ...
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Great! Additionally, I would like to always update the progress bars when the mouse is moved, but only draw in `QGraphicsView` when the left mouse button is pressed. I've added `if event.button() == Qt.LeftButton` to `mouseMoveEvent(self, event)` in `GraphicsView()` class, but it didn't work that way. – Atalanttore Dec 01 '19 at 20:19
  • The progress bars only seem to be updated when the mouse movements are made over a `QGraphicsView`. – Atalanttore Dec 01 '19 at 21:03
  • @Atalanttore According to the statement of your publication: *I would like to track the mouse movement for calculation (of values for progress bars) while the mouse is used to draw in one of the two QGraphicsView* it can be deduced that you want the tracking to be only in the QGraphicsView, mmm, do you want the tracking to be over the entire window? – eyllanesc Dec 01 '19 at 21:06
  • I've added a `MouseListener` for the window to track mouse movements over the entire window, but that didn't work out. See the update. – Atalanttore Dec 03 '19 at 22:09
  • 1
    @Atalanttore You must be direct when you write what you require, that you change or deepen your requirements after accepting the answer is not a good idea. For example, it causes a question-answer to never end which is not the objective of SO. In this case I will update my answer for your new requirement but I will do it only once. – eyllanesc Dec 03 '19 at 22:23
  • Thanks for your patience (and of course for your code). I constantly get new ideas while trying and learning new things in PyQt. In this case, I guess I exaggerated too much. I'll limit myself to understanding your code: 1. Why do you call the superclass methods at the end of `mousePressEvent` or `mouseMoveEvent` in the `GraphicsView` class? 2. Why do you also check for `if obj in [self._widget] + self._childrens` in the `eventFilter` of the class `MouseListener`? The program also works without these calls or conditions. See the Update 2. – Atalanttore Dec 04 '19 at 20:38
  • 1
    @Atalanttore 1) That you have more curiosities does not imply that your question is modified at any time, if you have new doubts then you have to create a new publication (and if you consider relevant then signals as antecedent the previous question/answer) – eyllanesc Dec 04 '19 at 21:12
  • 1
    @Atalanttore 2) When you override a method of father and you don't call super() you are eliminating the default behavior, for example if you enable the drag that is handled by mouseXEvent methods then by not calling super it probably won't work anymore, if you override the mousePressEvent method of a button and you don't call to super then the click signal would not fire. – eyllanesc Dec 04 '19 at 21:12
  • 1
    @Atalanttore 3) In your case, that part of the code is unnecessary since your widget is empty when I add the filter but suppose that after adding several widgets just add the filter, would the previous widgets be filtered? No, that's why I add the filter to existing children and also using the ChildX events trace the widgets that will be added. With the above, I give an end to the comments section because, as I already pointed out: In SO we do not want endless questions. – eyllanesc Dec 04 '19 at 21:12