Reliably showing a "please wait" dialog while doing a lengthy blocking operation in the main Qt event loop

StackOverflow https://stackoverflow.com/questions/22670564

Question

I've got a Qt app that needs to call an expensive non-Qt function (e.g. to unzip a ~200MB zip file), and since I'm calling that function from the main/GUI thread, the Qt GUI freezes up until the operation completes (i.e. sometimes for 5-10 seconds).

I know that one way to avoid that problem would be to call the expensive function from a separate thread, but since there isn't much the user can do until the unzip completes anyway, that seems like overkill. I can't add processEvents() calls into the expensive function itself, since that function is part of a non-Qt-aware codebase and I don't want to add a Qt dependency to it.

The only thing I want to change, then, is to have a little "Please wait" type message appear during the time that the GUI is blocked, so that the user doesn't think that his mouse click was ignored.

I currently do that like this:

BusySplashWidget * splash = new BusySplashWidget("Please wait…", this);
splash->show();
qApp->processEvents(); // make sure that the splash is actually visible at this point?
ReadGiantZipFile();    // this can take a long time to return
delete splash;

This works 95% of the time, but occasionally the splash widget doesn't appear, or it appears only as a grey rectangle and the "Please wait" text is not visible.

My question is, is there some other call besides qApp->processEvents() that I should also do to guarantee that the splash widget becomes fully visible before the lengthy operation commences? (I suppose I could call qApp->processEvents() over and over again for 100mS, or something, to convince Qt that I'm really serious about this, but I'd like to avoid voodoo-based programming if possible ;))

In case it matters, here is how I implemented my BusySplashWidget constructor:

BusySplashWidget :: BusySplashWidget(const QString & t, QWidget * parent) : QSplashScreen(parent)
{
   const int margin = 5;
   QFontMetrics fm = fontMetrics();
   QRect r(0,0,margin+fm.width(t)+margin, margin+fm.ascent()+fm.descent()+1+margin);
   QPixmap pm(r.width(), r.height());
   pm.fill(white);

   // these braces ensure that ~QPainter() executes before setPixmap()   
   {
      QPainter p(&pm);
      p.setPen(black);
      p.drawText(r, Qt::AlignCenter, t);
      p.drawRect(QRect(0,0,r.width()-1,r.height()-1));
   }
   setPixmap(pm);
}
Was it helpful?

Solution

Moving to another thread is the correct way to go but for simple operations, there's a much less complicated way to accomplish this without the pain of managing threads.

BusySplashWidget splash("Please wait…", this); 

QFutureWatcher<void> watcher;
connect(&watcher, SIGNAL(finished()), &splash, SLOT(quit()));

QFuture<void> future = QtConcurrent::run(ReadGiantZipFile);
watcher.setFuture(future);    

splash.exec(); // use exec() instead of show() to open the dialog modally

See the documentation about the QtConcurrent framework for more information.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top