Unsolved Parent and Child Handling of Events
-
I am trying to understand how to handle events by parent and child QWidgets. For example I would like a child QWidget to handle events and prevent them from being passed up to its parent. Reading the docs I thought if I return
True
from my event handler and/or callingevent.accept()
it would prevent the parent receiving the event signal however it seems whatever I try the parent always receive the event. I also tried inspectingevent.accepted()
but this seems to be set True regardless. The following is my example code:import logging import sys from typing import Optional import PySide6.QtWidgets from PySide6.QtCore import QEvent from PySide6.QtGui import QTabletEvent, QColor, Qt from PySide6.QtWidgets import QApplication, QMainWindow, QWidget class TestApplication(QMainWindow): def __init__(self) -> None: super().__init__() menuBar = self.menuBar() menuBar.addMenu("&File") menuBar.addMenu("&Edit") self.logger = logging.getLogger("TestApplication") def event(self, event: QEvent) -> bool: #self.logger.info("isAccepted: {}".format(event.isAccepted())) if not event.isAccepted() and (event.type() == QEvent.Enter or event.type() == QEvent.Leave): self.logger.info("Event: {}".format(event.type())) event.accept() return True return super(TestApplication, self).event(event) def tabletEvent(self, event: QTabletEvent) -> None: super().tabletEvent(event) class Canvas(QWidget): def __init__(self, parent: Optional[PySide6.QtWidgets.QWidget]) -> None: super().__init__(parent) self.logger = logging.getLogger("Canvas") self.setAutoFillBackground(True) p = self.palette() p.setColor(self.backgroundRole(), QColor(Qt.red)) self.setPalette(p) def event(self, event: QEvent) -> bool: self.logger.info("isAccepted: {}".format(event.isAccepted())) if event.type() == QEvent.Enter or event.type() == QEvent.Leave: self.logger.info("Event: {}".format(event.type())) event.accept() return True return super(Canvas, self).event(event) if __name__ == "__main__": app = QApplication() win = TestApplication() win.setCentralWidget(Canvas(win)) win.resize(500, 500) win.show() sys.exit(app.exec())
-
@D-Drum The documentation for that method says:
This virtual function receives events to an object and should return true if the event e was recognized and processed. The function can be reimplemented to customize the behavior of an object. Make sure you call the parent event class implementation for all the events you did not handle.
Where does it say: it would prevent the parent receiving the event signal?
-
@eyllanesc said in Parent and Child Handling of Events:
@D-Drum The documentation for that method says:
This virtual function receives events to an object and should return true if the event e was recognized and processed. The function can be reimplemented to customize the behavior of an object. Make sure you call the parent event class implementation for all the events you did not handle.
Where does it say: it would prevent the parent receiving the event signal?
My point is, no matter what I tried the parent always receives the event which is fine but I cannot distinguish between whether the child has already process the event. Try experimenting with my example. I never said the documents 'would prevent the parent receiving the event' .. I said I thought it would - else how else would the parent know the event had been processed or not, especially if the
event.accepted()
always seemed to be true?Please read the question and if you like experiment with my example e.g. just return
True
from the child handler and see the result. -
@D-Drum Each type of event has a special life cycle, and each "event" object is different from the one that was sent to another widget, for example the QEnterEvent event has a localPos method that has to be different for each widget since each one has its own coordinate system so if you accept the event in a widget it will not imply that isAccepted is true in another widget. That an event is consumed is not notified to another widget. For example, another case is the mousePress event where the event is sent from child to parent, and if a widget consumes it then the parent will no longer receive the event but will not be notified when the children consumed the event, isAccepted is handled internally by Qt and not by parent widgets.
-
@D-Drum Your requirement does not make sense that a parent widget has to be notified when the "enter" event occurs in a child widget, for example let's say the parent widget is 100x100 and the child 50x50 is centered on the parent. Let's say that the mouse is in the 10x10 position of the parent so the "enter" event of the parent has already fired but not yet in the child, and now let's say that the mouse moves towards the center so that at a given moment it is will trigger the "enter" event on the child, why would the parent be notified? It should not be notified as for the parent there is no such "enter" event as it was always within the parent
-
Your example is valid however that is not the scenario I have and demonstrated in the example and besides, according to the docs for QEvent.accept() 'Unwanted events might be propagated to the parent widget'.
In my example I have set my QMainWindow as the parent of the child window and I have found that the event is ALWAYS propagated to the parent. As far as I see it It doesn't matter if the child is smaller than it's designated parent. What is the point of returningTrue
orFalse
from the child parent handler or calling theaccept()
if it's going to be passed up to the parent and the parent cannot tell if it's child(ren) have handled the event or not? I'm assuming Pyside6 is using event bubbling - but I'm doing something wrong or I'm misunderstanding how it works in Pyside6. -
Looking at the C++ docs regarding the Event system (assuming these are valid for Pyside6) it would appear my understanding of how the event system works is correct.
event()
appears to be a 'catch all' method for all events but there are specialist functions that are for general use. For my case there areenterEvent()
andleaveEvent()
functions and I tried overriding these and callingaccept()
but still the parent is receiving events.Particularly for the
event()
method it states: 'Note that QWidget::event() is still called for all of the cases not handled, and that the return value indicates whether an event was dealt with; a true value prevents the event from being sent on to other objects.'Either I am interpreting the docs incorrectly, I'm making an error or something just isn't working correctly.
-
@D-Drum As I already pointed out: Each type of event has its own workflow.
Enter
In the case of
QEvent::Enter
it happens when the mouse cursor moves from the outer region to the inner region:import logging import random import sys from PySide6.QtCore import QEvent from PySide6.QtGui import QColor from PySide6.QtWidgets import QApplication, QWidget logging.basicConfig(level=logging.DEBUG) class Widget(QWidget): def __init__(self, *, name, parent=None): super().__init__(objectName=name, parent=parent) self.setAutoFillBackground(True) p = self.palette() p.setColor(self.backgroundRole(), QColor(*random.sample(range(255), 3))) self.setPalette(p) def event(self, event): if event.type() == QEvent.Enter: logging.debug(f"objectName: {self.objectName()}") return super().event(event) def create_widgets(): size = 512 widgets = [] parent = None for i in range(4): child = Widget(name=f"Child-{i}", parent=parent) child.setGeometry(size / 2, size / 2, size, size) widgets.append(child) parent = child size /= 2 return widgets if __name__ == "__main__": app = QApplication() widgets = create_widgets() widgets[0].show() sys.exit(app.exec())
That event
QEvent::Enter
is fired on the child does not imply that it should be fired on the parent, they are independent. In your example, what happens is that the borders are very close, so 2 independent events are triggered since neither depends on the other.MouseButtonPress
In contrast to other events that are dependent, for example the
QEvent::MouseButtonPress
event that happens when the mouse presses the interior region of a widget. On the other hand, the inner region of a child widget is always the inner region of a parent widget, so the event is fired from child to parent as shown in the following example.import logging import random import sys from PySide6.QtCore import QEvent from PySide6.QtGui import QColor from PySide6.QtWidgets import QApplication, QWidget logging.basicConfig(level=logging.DEBUG) class Widget(QWidget): def __init__(self, *, name, parent=None): super().__init__(objectName=name, parent=parent) self.setAutoFillBackground(True) p = self.palette() p.setColor(self.backgroundRole(), QColor(*random.sample(range(255), 3))) self.setPalette(p) def event(self, event): if event.type() == QEvent.MouseButtonPress: logging.debug(f"objectName: {self.objectName()}") return super().event(event) def create_widgets(): size = 512 widgets = [] parent = None for i in range(4): child = Widget(name=f"Child-{i}", parent=parent) child.setGeometry(size / 2, size / 2, size, size) widgets.append(child) parent = child size /= 2 return widgets if __name__ == "__main__": app = QApplication() widgets = create_widgets() widgets[0].show() sys.exit(app.exec())
DEBUG:root:objectName: Child-3 DEBUG:root:objectName: Child-2 DEBUG:root:objectName: Child-1 DEBUG:root:objectName: Child-0
Now if only one widget consumes the event and does not propagate to the parents then return True:
def event(self, event): if event.type() == QEvent.MouseButtonPress: logging.debug(f"objectName: {self.objectName()}") return True return super().event(event)
DEBUG:root:objectName: Child-3
Now the event is not propagated from child to parent.
Another way to do the same is by accepting the event in mousePressEvent:
class Widget(QWidget): def __init__(self, *, name, parent=None): super().__init__(objectName=name, parent=parent) self.setAutoFillBackground(True) p = self.palette() p.setColor(self.backgroundRole(), QColor(*random.sample(range(255), 3))) self.setPalette(p) def event(self, event): if event.type() == QEvent.MouseButtonPress: logging.debug(f"objectName: {self.objectName()}") return super().event(event) def mousePressEvent(self, event): event.accept()
KeyPress
For example, another case where events are dependent is that of
QEvent::KeyPress
, which happens when the focus is pressed or the window has focus. This is fired from the widget that has the focus to its parents.import logging import random import sys from PySide6.QtCore import QEvent from PySide6.QtGui import QColor from PySide6.QtWidgets import QApplication, QWidget logging.basicConfig(level=logging.DEBUG) class Widget(QWidget): def __init__(self, *, name, parent=None): super().__init__(objectName=name, parent=parent) self.setAutoFillBackground(True) p = self.palette() p.setColor(self.backgroundRole(), QColor(*random.sample(range(255), 3))) self.setPalette(p) def event(self, event): if event.type() == QEvent.KeyPress: logging.debug(f"objectName: {self.objectName()}, hasFocus?: {self.hasFocus()}") return super().event(event) def create_widgets(): size = 512 widgets = [] parent = None for i in range(4): child = Widget(name=f"Child-{i}", parent=parent) child.setGeometry(size / 2, size / 2, size, size) widgets.append(child) parent = child size /= 2 return widgets if __name__ == "__main__": app = QApplication() widgets = create_widgets() widgets[0].show() sys.exit(app.exec())
DEBUG:root:objectName: Child-0, hasFocus?: False
As there was no widget with focus then the toplevel consumed it.
But if we set the focus on the widget that has no children then the event is sent to its parents:
if __name__ == "__main__": app = QApplication() widgets = create_widgets() widgets[0].show() widgets[-1].setFocus() sys.exit(app.exec())
DEBUG:root:objectName: Child-3, hasFocus?: True DEBUG:root:objectName: Child-2, hasFocus?: False DEBUG:root:objectName: Child-1, hasFocus?: False DEBUG:root:objectName: Child-0, hasFocus?: False
Now if we want to avoid propagation then it is enough to return True in the event method:
def event(self, event): if event.type() == QEvent.KeyPress: logging.debug(f"objectName: {self.objectName()}, hasFocus?: {self.hasFocus()}") return True return super().event(event)
DEBUG:root:objectName: Child-3, hasFocus?: True
Or accept the event in keyPressEvent:
class Widget(QWidget): def __init__(self, *, name, parent=None): super().__init__(objectName=name, parent=parent) self.setAutoFillBackground(True) p = self.palette() p.setColor(self.backgroundRole(), QColor(*random.sample(range(255), 3))) self.setPalette(p) def event(self, event): if event.type() == QEvent.KeyPress: logging.debug(f"objectName: {self.objectName()}, hasFocus?: {self.hasFocus()}") return super().event(event) def keyPressEvent(self, event): event.accept()
DEBUG:root:objectName: Child-3, hasFocus?: True
-
@eyllanesc
Thanks for taking the time to put together your detailed response. I've spent the last week investigating this further and duplicating my Python code into C++. However I've come to the conclusion thatMouseEnter
andMouseLeave
events are treated differently to at leastMouseButtonPress
andMouseButtonRelease
events.I can partly accept that the events for each mouse enter/leave are distinctly separate calls. This is not because as you suggested that the borders are very close; they are the same and the child is on top of the parent and therefore crossing one crosses both which results in two separate events. However where these events vary from mouse press/release is that mouse press/release events can be prevented from bubbled up to the parent by calling
event.accept()
and returningTrue
. I have verified this with said mouse events. but no matter what I try, I could never get the child mouse enter/leave events to bubble up to the parent. For example jJust using simple logging I would expect to see 3 logging statements per mouse entry/exit event; One for the parent, a second for the child and then a third from the parent when the child does not process the event but pass it up the chain.I've tried to consider why it would be like this and the only reason I can think of is to handle the very case I need to; that is where the child has its border directly above its parent. If the parent were to receive two events i.e. it's own direct mouse enter/leave and then the unprocessed child mouse enter/leave as far as I am aware there is no way to distinguish where the event originated and generally speaking any subsequent action would only be required once, not twice. But this could easily be resolved if the enter/leave events were handled in the same manner as the mouse press/release events. If the runtime is capable of distinguishing the child from the parent for mouse press/release events (presumably using z ordering) then why can't it do so in the case of mouse enter/leave events?
I've been looking at this issue for too long now so it's difficult to get out of my train of thought but if anyone has any other explanation I'd be happy to hear it.