Unpredictable exec()

Do you recognize this pattern?


void MyWidget::contextMenuEvent(QContextMenuEvent *event)
{
    if (conditionsAreMet) {
        QMenu menu;
        menu.addAction(action1);
        menu.addAction(action2);
        menu.addAction(action3);
        QAction *selectedAction = menu.exec(event->pos());
        doSomething(selectedAction);
    }
}

It's one of the more established patterns in Qt, nice and easy on the eye. But it hurts to say that this is one of the patterns that can lead to unpredictable flow of logic and chaos when debugging. The reason is that it gives the impression that exec() blocks everything until the menu, or dialog, is closed. And that's sort of the case, but in reality Qt "indiscriminately" continues to process events while the dialog is shown. So you can get (unexpected) event function calls, and slot invocations, while waiting for input from a menu or dialog.

main() => Qt's eventLoop() => contextMenuEvent() => Qt's eventLoop() => timerEvent()

(if timerEvent opens a dialog with exec)

main() => Qt's eventLoop() => contextMenuEvent() => Qt's eventLoop() => timerEvent() => Qt's eventLoop()

So what can this unexpected recursion lead to? From looking at the code at the top, it's easy to think that your whole app is basically blocked while the menu or dialog is shown. But this isn't the case. Only local code execution stops (as it usually does when you call a function that has not returned yet). Here are some examples of what can happen:

* Network data can arrive (readyRead() slot invocation)
* Timers can fire (timerEvent() can be called)
* You may recurse into the same event handler over again (e.g., don't open a QMessageBox::critical in a slot connected to QTcpSocket::bytesWritten())

For QMenu, two mechanisms prevent same-eventhandler recursion, and this covers 95% of the problems that could occur. One is that QMenu is sort of modal. That means that it sort of takes over mouse activity while it's visible, which makes it inherently hard to right-click on the widget which is now below it without QMenu getting the event instead. If you could, then a second menu would pop up. That's one. The second mechanism is that because QMenu is a window, the window with the MyWidget instance on it loses the mouse grab. So even if you opened that menu while receiving mouse move events (which typically come in high numbers as you move the mouse around), the moment the menu is open, the originating MyWidget instance stops getting mouse events. It also loses focus temporarily, so you can't invoke the keyboard handlers either (context menus events can be triggered from pressing the funny key on your keyboard with a pictogram of a keyboard). And there's some code to make sure you can't open a new top level menu while another is already open.

So, Qt has covered most cases where reentering the event loop could cause a problem: you can't send mouse and key input to the originating widget. For menus. But what about non-modal dialogs? For example, opening a QFileDialog when somebody presses the ellipsis button (Browse...). If that dialog isn't modal, then you _can_ press the button again. And again. Fun! ;-)


void MyWidget::mousePressEvent(QMouseEvent *event)
{
    // if this dialog was non-modal, you could just press the mouse again
    QString dir = QFileDialog::getExistingDirectory(...);
}

Qt has covered this problem as well. QFileDialog's static (exec-like) functions, which also reenter the event loop, always create a modal file dialog (steals focus and stops mouse events to parents). Qt to the rescue!

Conclusion:

Reentering the event loop is evil, but normally causes no harm. In Qt 4.5 we added alternative functions to our dialogs. The open() pattern was introduced:


void MyWidget::contextMenuEvent(QContextMenuEvent *event)
{
    if (conditionsAreMet) {
        QColorDialog *dialog = new QColorDialog;
        dialog->open(this, SLOT(dialogClosed(QColor)));
    }
}

void MyWidget::dialogClosed(const QColor &color)
{
    colorize(color);
}

By using open() instead of exec(), you need to write a few more lines of code (implementing the target slot). But what you gain is very significant: complete control over execution. Now, the event loop is no longer nested/reentered, you're not blocking inside a magic exec() function, and it's clear that it's the modality state of the dialog that decides how you can interact with the MyWidget instance. And because it makes sense, the dialogs are Window Modal by default.


Blog Topics:

Comments