Unpredictable exec()

Published Tuesday February 23rd, 2010
9 Comments on Unpredictable exec()
Posted in Qt

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.

Do you like this? Share it
Share on LinkedInGoogle+Share on FacebookTweet about this on Twitter

Posted in Qt

9 comments

Albert says:

The declaration of your slot does not match the call to open πŸ™‚

Ivan says:

My congratulations on removisng modal shit!

trenton says:

Glad you shed more light on this, Andreas! I also tried to do my part by trying to explain it in Qt Quarterly 30, New Ways of Using Dialogs:

http://doc.trolltech.com/qq/QtQuarterly30.pdf

I framed it in a Mac-centric “doing sheets is too hard in Qt” since that was our main goal at the time, but the cleaner event loops was definitely a secondary goal as we had to deal with that inside of the Cocoa event loop. It was much harder to make people think about not using exec() back then though (not only in Qt).

Anyway, here’s hoping that the open() paradigm becomes more adopted in Qt applications. It’s a good pattern.

jfriesne says:

Another problem I’ve observed with the modal-exec() pattern: if, while the Qt event loop is executing inside of exec(), something (e.g. a received network message) causes the widget that called exec() to be deleted, the program will probably crash, because when exec() returns, the method it returns to will have an invalid (freed) “this” pointer. Just say no to exec()! :^)

Andreas says:

@trenton: Wow, I had completely forgotten about this article. Thanks for pointing it out! πŸ™‚ It explains the problem and solution with greater detail.

Kari says:

Your suggestion to avoid exec() in menus and dialogs is great and I think it also solves another danger in exec().. see frank osterfeld’s article how quitting an application using D-Bus will most likely cause a crash, if the application was displaying a dialog (http://www.kdedevelopers.org/node/3919). It talks about KDE, but can happen on Qt apps, too.

Here’s an example:

SomeDialog* dlg = new SomeDialog( this );
if ( dlg->exec() == QDialog::Accepted ) {
const QString str = dlg->someUserInput();
}

If the app is waiting in dlg->exec(), then this crashes if a D-Bus signal to quit the app is received and the main window is destroyed. Destroying the main window causes all child widgets to be destroyed, including the dialog. So, dlg->someUserInput() is no longer valid. The solution is to use QPointers. I think properly used dlg->open() will solve it, too.

Ihhano says:

QApplication::ProcessEvents can do the same in complicated path))

Thomas Zander says:

@Ihhano QApplication::ProcessEvents essentially has the same *problems* as exec has. You probably want to avoid that one just as much πŸ™‚

michael says:

> http://doc.trolltech.com/qq/QtQuarterly30.pdf – “New Ways of Using Dialogs”

So Qt discovers “live feedback dialogs” in 2009 – wow. Gnome’s instant-apply is now also what, eight years old already?

Commenting closed.

Get started today with Qt Download now