Keyboard navigation in a GUI application might not be the most highlighted feature. Most devices use clickable or touchable controls in the user interface to change screens, open menus or activate buttons. Many developers don’t even bother looking at the problem because the customers might not ask for it. But if said application is intended for a device that doesn’t have other kind of input, or you have to satisfy some accessibility requirements, it’s definitely something that will end up being the feature that blocks a release if it has some known bug.

First we will cover some interesting details of Qt’s event system, and then we will move to the details that you should know for providing your UI with a good keyboard navigation system.

Qt’s event system

The usual killer feature in Qt that most people recall is the signal and slot mechanism. The event system, however, powers under the hood the killer feature of the killer features: the thread-safe delivery of signals to objects in different threads. If you use Qt’s events extensively, you will probably end up being more in love with the event system (even though, of course, signals and slots are used more extensively, since it is a higher level concept).

Most often you don’t send events. You receive the ones created by Qt. But there are two ways to produce your own events:

  • QCoreApplication::postEvent: Asynchronous. Posts to the queue and returns immediately.
  • QCoreApplication::sendEvent: Synchronous. Sends the event immediately, and returns when the event is received and processed by the destination (and returns the result returned by the event handler).

Both are implemented using QCoreApplication::notify, which is specialized (it’s a virtual function) by the two QCoreApplication subclasses in Qt (QGuiApplication and QApplication) or by the user. Note that there is a warning in the API documentation about the possible deprecation of notify in Qt 6, so take that in mind if you intensively plan on using notify for your own event handling.

From application object to specific object

Besides some internal details, notify ends up doing the following:

  • Calling the event filters in the application instance.
  • Calling the event filters in the receiver.
  • Calling the receiver object directly.

If any event filter returns true, it is understood as the filter having consumed the event, and notify will stop immediately. Notify will also return true, because it expresses that the event was received by someone (the filter instead of the object in this case, but someone nevertheless).

The receiver object will be called like this: receiverObject->event(eventObject);. The “event” function here, being of course QObject::event. That’s another virtual function that you can implement to handle events.

In case you are not familiar with event filters, it’s a very simple concept. By calling installEventFilter on any QObject, and passing a second QObject (which can be the same or different) as filter, you get the opportunity so see events sent to one object (or to the whole application) before said object has any knowledge of them. You can inspect them (a powerful debugging mechanism), and let the event pass to the receiver.  You can also decide that the filter object handles the event, either by reacting to it or by blocking it.

This feature can be critical for the propagation of events in widgets, that we will explain below.

The event reaches the receiver

Now we get to the third way of seeing events before the usual implementation: the QObject::event function.

The implementation in QObject or QWidget is a large switch-case construct, that asks the event for it’s type, and decides which specialized function will really handle it. If the event type is a QEvent::PaintEvent, it is cast to a QPaintEvent and delivered to the QWidget::paintEvent function. If the event type is a QEvent::Timer, it is cast to a QTimerEvent and delivered to the QObject::timerEvent function. As simple as that.

There is little more to understand about the implementation of QWidget::event, except from the case that matters entirely for us now: the handling of the keyboard navigation.

Changing focus

The place in Qt where the focus is changed for you (if you have not implemented something else yourself) is in QWidget::event. The code is very short:

case QEvent::KeyPress: {
    QKeyEvent *k = (QKeyEvent *)event;
    bool res = false;
    if (!(k->modifiers() & (Qt::ControlModifier | Qt::AltModifier))) {  //### Add MetaModifier?
        if (k->key() == Qt::Key_Backtab
            || (k->key() == Qt::Key_Tab && (k->modifiers() & Qt::ShiftModifier)))
            res = focusNextPrevChild(false);
        else if (k->key() == Qt::Key_Tab)
            res = focusNextPrevChild(true);
        if (res)
            break;
    }
    keyPressEvent(k);

Unfortunately this might not be flexible enough for us. We can’t tune that code to change the focus using, for example, the cursor keys, or the function keys, or some key specific to our hardware (if you are using Qt for some custom device). But as you see, not too much code is required to do your own navigation with hardware keys.

So widgets will move the focus to the next sibling on presses of the tab key, or to the previous when the key is backtab. Yes, you read sibling, and the function above is focusNextPrevChild. That is because by default, non-window widgets will propagate the call to their parent to focus the next child.

This is an important function that you should be aware of, because if you need to change the focus using a different strategy than what Qt has, this function is a good place to control it.

The focus chain

In the previous code block we saw how the tab and backtab keys are a bit hardcoded in Qt to handle the focus change. There is one place where “tab” is even more hardcoded: in the name of the setTabOrder function! You can change the focus chain for your application, which might not support the tab key at all, but you still need to call a function with the “tab” name on it.

OK, that is not a big deal, but it still is a bit confusing when you look at the API. The “setter” for the focus chain is called setTabOrder, while the “getter” is nextInFocusChain or previousInFocusChain.

The hidden keyboard navigation

If you found yourself  looking at Qt’s source code at the places mentioned in this blog post, your eyes probably will have caught the presence of some blocks of code behind #ifdef QT_KEYPAD_NAVIGATION. Indeed, Qt supports a more rich keyboard navigation that can move around the interface in the usual four directions. But it’s not compiled in by default, so it is not going to be convenient for many development teams, Linux distributions, and is not going to be as nicely tested by Qt developers (or users).

So it’s likely that you will implement your own, but it is still a very interesting read if you need to support something like this. The gist of it is that you need to play with the geometries of the widgets and their placements, and create a function that can give you the widget in a certain direction that fulfills your requirements.

Your designer or customer might ask that the focus should “wrap around the edges” (which means, that, for example, a left movement on the leftmost widget, places the focus on the rightmost). Or they might ask that a widget is only in the same line if the two align perfectly. Just remember that QWidget::mapToGlobal(QPoint()) is going to be really useful to compare rectangles with the same global coordinates.

Handling keystrokes and propagation

Handling a key event is simple, and the way most people get exposed to events for the first time, but it’s worth quickly refreshing it, especially the event propagation. It’s easy to overlook it when searching for problems, because it’s an easy concept.

If you want to handle in your widget some key press or key release, you most often do it by implementing one QWidget virtual function in your class (keyPressEvent or keyReleaseEvent). If you don’t do it, the default implementation in QWidget just does event->ignore(). That means that the widget is not interested in the event, and in the case of key (and mouse) events, something useful happens: the event gets propagated to the parent of the widget.

That is not the case in most events. When one object receives, for example, a timer event, the event doesn’t get propagated to the parent. There is only one possible object that receives that type of event. In GUI applications, it is common to have event propagation (or “bubbling”) of user input events (keyboard and mouse). So be careful in the implementation of your key handlers and do something like the following:

void MyWidget::keyPressEvent(QKeyEvent* event)
{
    if (event->key() == TheKeyToHandle) {
        // The next line is not really needed: accept it's the default.
        event->accept();
        doSomething();
    }
    else {
        // The next two lines are equivalent. Use only one, and do it
        // consistently, but do it at least once if you don't handle the event.
        QWidget::keyPressEvent(event);
        event->ignore();
    }
}

To summarize:

  • If you don’t implement the event handler, the default is called, which ignores the event, and will cause propagation to the parent.
  • If you do implement the handler, and don’t call event->ignore() directly or indirectly, the event will be accepted, and propagation won’t happen.
  • Events are propagated from child to parent. If you need the event to reach the parent first, you should make the parent the event filter. Using (or abusing, depending on who you ask) QStyle::polish(QWidget* widget) for this is one possibility.

Conclusion

With the above information you should now have all the knowledge to implement keyboard navigation in your application. Make sure you know all the features well and think carefully how to structure the event handling. If you end up using a lot of event filters, you are probably doing it wrong. Event filters, specially if they are in the application object, will be expensive. Remember that the application will receive many events that are not just key and mouse events, and that all the application event filters will be called on those as well.