19. Events

In the Qt framework, events are objects derived from the QEvent class that represent things that have happened either within an application or as a result of outside activity and that require handling. Events are especially relevant for Qt widgets, as they allow widgets to react to changes such as mouse movements, keyboard presses, timers firing, or object deletion. The event system relies on Qt’s event loop, which processes queued events and dispatches them to the appropriate QObject receivers. Developers can customize event handling by reimplementing virtual event methods in subclasses, providing fine-grained control over application behavior. Common event types include QMouseEvent for pointer interactions, QKeyEvent for keyboard actions, and QPaintEvent for rendering updates[^1].

There are five ways events can be processed:

  1. Reimplementing type-specific event handler methods like mousePressEvent().
  2. Reimplementing the QObject.event() method.
  3. Installing an event filter on the object.
  4. Installing an event filter on the QCoreApplication instance.
  5. Reimplementing the QCoreApplication.notify() method[^2].

19.1 Event Handlers

A Qt class’s main event handler is the event() method. When an event occurs, this method receives a QEvent object representing it. Typically, it does not handle the event itself but instead calls the appropriate type-specific event handler. You can reimplement the base class’s event handler completely, in which case you must implement all handling yourself, or handle only certain event types and call the base class method for others.

An icon of a clipboard-list

Whenever your application’s main window becomes visible on the screen, add it to the list of visible windows. When it becomes hidden, remove it from the list. Your additional task is to record and suppress key press events when the user presses the letter ‘q’.

To use event handlers in your application:

  1. Reimplement the keyPressEvent() method. To suppress the ‘q’ key presses, check if event.text() equals ‘q’. If it does, record it in a label. Since QKeyEvent.isAccepted() is True by default, this suppresses the ‘q’ key presses. For all other key presses, call the base class keyPressEvent().

  2. Reimplement the showEvent() method. Simulate adding the window to a visible windows list by recording the event in the label, then call the base class showEvent().

  3. Reimplement the hideEvent() method. When the window is hidden, the label is also hidden, so simulate removing the window from the visible windows list by printing a message. Then call the base class hideEvent() for further processing.

The event() method dispatches events to type-specific event handlers like showEvent() or keyPressEvent(). While you can use it to handle events directly, the documentation recommends using type-specific handlers instead. In the example, we intercept key press and show events in event() and print a message for each, then call the base class event() for further processing. You can also use event() to suppress events - if you uncomment the two return True statements, events never reach keyPressEvent() and showEvent().

19.2 Object Event Filters

In the previous section we saw how to intercept events by reimplementing a QObject’s event() method before the events reach the type-specific event handlers. Sometimes, however, you need to monitor or suppress events destined for objects that you cannot (or do not want to) subclass in order to reimplement event(). This is achieved by installing an event filter.

An icon of a clipboard-list

Your task is to log mouse press events for a group of buttons while completely suppressing clicks on one particular button.

To use an event filter in your application:

  1. Create a class that inherits from QObject and reimplement its eventFilter() method. The signature is:
1 def eventFilter(self, watched: QObject, event: QEvent) -> bool

The watched parameter refers to the object receiving the event. In this example, we check for MouseButtonPress events, log them to the console, and return True for button2 to consume the event (preventing further processing, including the clicked() signal). For all other cases, we return the result of the base class implementation.

  1. Instantiate the event filter object (in this case, a MousePressFilter instance named event_filter).

  2. Install the event filter on each target widget using installEventFilter().

If you delete the watched object inside eventFilter(), you need to return True, otherwise the application might crash.

The output is:

1 Filter mouse press for:  button1
2 Button clicked:  button1
3 Filter mouse press for:  button2
4 Filter mouse press for:  button3
5 Button clicked:  button3

Consuming an event means stopping it from being processed further. The clicked() signal requires Qt to process both a mouse press and a mouse release on the button. By consuming the MouseButtonPress event in the filter, the button never receives the press event, so it can’t recognize a complete click action.

Event filters can be chained, meaning you can install multiple filters on the same object. When an event occurs, Qt calls the filters in reverse order of installation.

19.3 Application-Wide (Global) Event Filters

In the previous section, we installed an event filter on individual buttons. A global event filter, installed on the QApplication object, can intercept all events for all widgets in the application - including mouse events that would normally be ignored by disabled widgets.

An icon of a clipboard-list

You want users to be able to “click” disabled buttons (i.e., trigger their clicked signal) without re-enabling them visually or functionally.

To implement a global event filter:

  1. Create the event filter class. Reimplement eventFilter() to detect MouseButtonPress events. If the target widget (the watched parameter) is a disabled QPushButton, manually emit its clicked() signal and consume the event by returning True.

  2. Instantiate the filter object, passing the QApplication instance as its parent.

  3. Install the filter on the QApplication instance using QApplication.installEventFilter().

Sample console output when clicking each button in order:

1 Filter mouse press for:  Main WindowWindow
2 Filter mouse press for:  button1
3 Button clicked:  button1
4 Filter mouse press for:  Main WindowWindow
5 Filter mouse press for:  button2
6 Button clicked:  button2
7 Filter mouse press for:  Main WindowWindow
8 Filter mouse press for:  button3
9 Button clicked:  button3

Note that the filter receives each mouse press event twice. Qt delivers them first to the QWindow instance associated with the application top-level window (‘Main WindowWindow’), then to the specific button under the mouse. Since the filter is installed globally, it intercepts the event at both stages.

This approach bypasses Qt’s normal disabled-state behavior. Disabled buttons will emit clicked() signals but won’t show visual press feedback. Application-wide filters see every event in your application, so keep the eventFilter() method efficient to avoid performance issues.

19.4 Event Propagation

The QCoreApplication.notify() method delivers all events to their receiver objects. Certain event types, such as mouse and key events, propagate up the parent widget chain if the receiver ignores them (i.e., does not accept the event).

An icon of a clipboard-list

Your application requires users to create an account. To better understand how they interact with the “Create account” dialog (for usability testing or debugging) you want to log every mouse click and keyboard press, including which widget received the event and how events propagate when not handled.

To implement a global activity logger:

  1. Create an EventFilter class inheriting from QObject. In its eventFilter() method, detect MouseButtonPress and KeyPress events and log them to the console.

  2. In the main section, after creating the QApplication, instantiate the filter and install it globally using QApplication.installEventFilter().

  3. Create the main window as a QWidget containing:

    • A QLineEdit for entering the user name,
    • A QLabel below it displaying the generated e-mail address. Make the text selectable so users can copy it.
    • A checkbox to enable email notifications.

The widgets are nested inside two QGroupBox containers to create a clear parent hierarchy.

Here is a sample output from typical interactions:

 1 Log: key pressed in  MainWindowWindow
 2 Log: key pressed in  line_edit
 3 Log: mouse pressed in  MainWindowWindow
 4 Log: mouse pressed in  checkbox
 5 Log: mouse pressed in  MainWindowWindow
 6 Log: mouse pressed in  label
 7 Log: key pressed in  MainWindowWindow
 8 Log: key pressed in  label
 9 Log: key pressed in  inner_groupbox
10 Log: key pressed in  outer_groupbox
11 Log: key pressed in  MainWindow

Because the filter is installed globally, it sees events at every step of delivery and propagation. Interactive widgets like the line edit and checkbox accept most events they receive, stopping further propagation. The label behaves differently: it accepts mouse presses but ignores most key presses. As a result, key events on the label propagate up through the inner group box, the outer group box, and finally the main window.

As in section 19.3, the QWindow object named ‘MainWindowWindow’ is the first to receive the event.

19.5 Custom Events

Qt allows you to define and post your own event types, extending the event system for application-specific needs. Custom events are processed in the event loop like built-in events and can be handled in the customEvent() handler, event(), or via event filters.

To create a custom event:

  • Register a unique type with QEvent.registerEventType().
  • Subclass QEvent and add data attributes.
  • Post instances to receivers using QApplication.postEvent(receiver, event).
An icon of a clipboard-list

Your task is to create a custom Qt event whenever the current CPU usage percent exceeds a threshold. The event should hold the CPU usage percent as the data.

To create a custom Qt event that represents CPU usage changes:

  1. Define a custom event class CpuUsageEvent that inherits from QEvent and carries the used CPU percent value. Use QEvent.registerEventType() to assign a unique type to the class.

  2. Use a QTimer with an interval of 1000 ms to periodically poll the CPU usage percent with psutil.cpu_percent.

  3. If the CPU percent exceeds the threshold, create an event and add it to the event queue via QApplication.postEvent().

Note that custom events do not automatically propagate up the widget hierarchy like mouse or key events.

19.6 Event Delivery Order

In the chapter introduction we listed five different ways that events can be processed. So far we have covered four of them, each more powerful than the previous one:

  • Type-specific event handler methods like keyPressEvent(), capable of intercepting a single event type of a single Qt class.
  • The main event handler (event()), capable of intercepting all event types for a Qt class that reimplements it.
  • Object event filters, which can intercept all event types for the object on which they are installed (which is not necessarily the object in which they are defined).
  • Global event filters, that can intercept all events for all widgets in the application.

What we haven’t seen is an example of the fifth way, reimplementing QCoreApplication.notify(). If you look up this method in the documentation you will find this warning:

Future direction: This function will not be called for objects that live outside the main thread in Qt 7. Applications that need that functionality should find other solutions for their event inspection needs in the meantime. The change may be extended to the main thread, causing this function to be deprecated.

Reimplementing notify() gives you complete control, and is as powerful as installing a global event filter, but may be deprecated in the future.

Having multiple event-handling mechanisms like these at your disposal lets you exercise fine-grained control over your application’s events, but also presents you with a problem: they can all be used simultaneously, and you need to be aware of the order in which an event is processed by each of them. Examining event delivery order is the subject of this section.

Based on their origin, Qt events can be either:

  • Spontaneous - originating outside the application (e.g., from the window system).
  • Non-spontaneous - generated within the application (e.g., by a QTimer).

Event delivery order differs somewhat depending on the event type. Let’s first examine how non-spontaneous events are delivered.

The first script tracks every QTimerEvent as it passes through Qt’s five event-delivery layers. To make the path visible we simply call a small reporting function at each layer:

1 def report_layer(layer, receiver, event):
2     print(layer,
3           f'rec: {receiver.objectName()}, ',
4           f'evt id: {id(event)}, ',
5           f'timer id: {event.timerId()}')

The function prints the layer name, the receiver’s objectName(), the Python identity of the event object, and the Qt timer ID.

To intercept QApplication.notify(), we subclass QApplication as TracingApplication and reimplement notify(). For the two filter layers we create two QObject subclasses, GlobalEventFilter and ObjectEventFilter, each reimplementing eventFilter(). Finally, we create a QLabel subclass named TrackedLabel and reimplement event() and timerEvent() in both TrackedLabel and TimerWidget, the main application window. All five points of interception:

  • TracingApplication.notify(),
  • GlobalEventFilter.eventFilter(),
  • ObjectEventFilter.eventFilter(),
  • TrackedLabel.event(),
  • TrackedLabel.timerEvent(),

filter out the timer events (event.type() == QEvent.Type.Timer), call report_layer(), and forward the event to the next layer. In TimerWidget.__init__() we:

  • Create a TrackedLabel and add it to the TimerWidget layout.
  • Install an object event filter on the label.
  • Start a timer on the label with startTimer() - it is the label that is the receiver from this point on.

In the application entry point we install a global event filter on the QApplication instance, and separately install that same object filter on the widget’s associated QWindow object (window.windowHandle()).

Running the application produces output similar to:

1 Timer id 1 started
2 
3 1. notify()       - rec: label,  evt id: 2431584320384,  timer id: 1
4 2. Global filter  - rec: label,  evt id: 2431584320384,  timer id: 1
5 3. Object filter  - rec: label,  evt id: 2431584320384,  timer id: 1
6 4. event()        - rec: label,  evt id: 2431584320384,  timer id: 1
7 5. timerEvent()   - rec: label,  evt id: 2431584320384,  timer id: 1

A timer event is created and passes through the five layers in fixed order:

  1. QApplication.notify().
  2. Global event filter.
  3. Object-level event filter.
  4. The label’s main event handler (TrackedLabel.event()).
  5. The type-specific event handler (TrackedLabel.timerEvent()).

Two things are worth noting about what doesn’t fire here. TimerWidget also reimplements event() and timerEvent(), but neither ever runs - the timer belongs to the label, so TimerWidget is simply never the receiver. Separately, the object filter installed on TimerWidget’s QWindow never runs either, for an unrelated reason: a QTimerEvent is generated entirely within Qt’s own event loop and delivered directly to whichever object started the timer, with no QWindow involved at any point.

Let’s now see how spontaneous events are delivered. The second script tracks QMouseEvents as they are delivered to a button that reimplements event() and mousePressEvent() (TrackedButton):

The implementation of all five layers is the same as in the first script, except that they now filter out mouse events (event.type() == QEvent.Type.MouseButtonPress). Instead of a timer ID, report_layer() logs the mouse position as x and y coordinates.

When you run the script and click the button you will see output like:

1 1. notify()          - rec: mouse_widgetWindow,  id: 1971726336704,  pos: (69, 24)
2 2. Global filter     - rec: mouse_widgetWindow,  id: 1971726336704,  pos: (69, 24)
3 3. Object filter     - rec: mouse_widgetWindow,  id: 1971726336704,  pos: (69, 24)
4 1. notify()          - rec: Button,  id: 1971786877312,  pos: (58, 13)
5 2. Global filter     - rec: Button,  id: 1971786877312,  pos: (58, 13)
6 3. Object filter     - rec: Button,  id: 1971786877312,  pos: (58, 13)
7 4. event()           - rec: Button,  id: 1971786877312,  pos: (58, 13)
8 5. mousePressEvent() - rec: Button,  id: 1971786877312,  pos: (58, 13)

An event with the id 1971726336704 and position relative to the window (69, 24) is created with MouseWidget’s QWindow (‘mouse_widgetWindow’) as the receiver and goes through the first three layers:

  1. notify().
  2. Global filter.
  3. Object filter. The last two layers (event() and mousePressEvent()) are not detected, since we never subclass QWindow to intercept them. After this first cycle, a new event is created with the button as the receiver, id 1971786877312, and a position (58, 13) now relative to the button rather than the window. For this second event, all five layers are reported.

Note that MouseWidget, like TimerWidget before it, also reimplements event() and mousePressEvent() - and, just like TimerWidget’s copies, neither ever fires. The widget itself is never the receiver of a mouse click; only its child button is. All five reported layers above belong to TrackedButton, not MouseWidget.

Finally, the third script follows QKeyEvents delivered to a line edit.

The setup mirrors the first two scripts. KeyWidget contains a single TrackedLineEdit, a QLineEdit subclass reimplementing event() and keyPressEvent(). KeyWidget itself also reimplements both methods. report_layer() now logs the key code in place of a position.

Typing a letter produces output comparable to the button click from the second script - there is a partially detected delivery to the QWindow, then a fully detected delivery to the line edit:

1 1. notify()          - rec: key_widgetWindow,  id: 2180534557376,  key: 65
2 2. Global filter     - rec: key_widgetWindow,  id: 2180534557376,  key: 65
3 3. Object filter     - rec: key_widgetWindow,  id: 2180534557376,  key: 65
4 1. notify()          - rec: LineEdit,  id: 2180534557376,  key: 65
5 2. Global filter     - rec: LineEdit,  id: 2180534557376,  key: 65
6 3. Object filter     - rec: LineEdit,  id: 2180534557376,  key: 65
7 4. event()           - rec: LineEdit,  id: 2180534557376,  key: 65
8 5. keyPressEvent()   - rec: LineEdit,  id: 2180534557376,  key: 65

QLineEdit accepts a regular letter key, so nothing propagates. Hitting the Escape key is different - QLineEdit has no use for it, so it’s still unaccepted once the line edit’s keyPressEvent() returns:

 1 1. notify()          - rec: key_widgetWindow,  id: 2711068596864,  key: 16777216
 2 2. Global filter     - rec: key_widgetWindow,  id: 2711068596864,  key: 16777216
 3 3. Object filter     - rec: key_widgetWindow,  id: 2711068596864,  key: 16777216
 4 1. notify()          - rec: LineEdit,  id: 2711068596864,  key: 16777216
 5 2. Global filter     - rec: LineEdit,  id: 2711068596864,  key: 16777216
 6 3. Object filter     - rec: LineEdit,  id: 2711068596864,  key: 16777216
 7 4. event()           - rec: LineEdit,  id: 2711068596864,  key: 16777216
 8 5. keyPressEvent()   - rec: LineEdit,  id: 2711068596864,  key: 16777216
 9 2. Global filter     - rec: key_widget,  id: 2711068596864,  key: 16777216
10 3. Object filter     - rec: key_widget,  id: 2711068596864,  key: 16777216
11 4. event()           - rec: key_widget,  id: 2711068596864,  key: 16777216
12 5. keyPressEvent()   - rec: key_widget,  id: 2711068596864,  key: 16777216

The event id is identical across all twelve lines. The same event object is delivered to the line edit. Once the line edit leaves it unaccepted, the same event object propagates to its parent, key_widget.