1

Problem

I want to use a QTimer to update a derived QSplashScreen that draws a progress bar (using paint commands, not a widget) to estimate when the program will start running.

By necessity, this happens prior to the exec call of the QCoreApplication. I've gotten this to work (in release mode only) on both X11 and Windows, by putting a timer in a second thread, and calling a function in the splash screen which updates the progress and repaints the widget. However, this doesn't work in debug mode as it produces the following error:

"ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread."

I'm not really worried about this assertion as the code doesn't crash in release and it's just a splash screen, however I need to be able to run the program in debug so I'd like to either a) refactor the code so it doesn't trigger the assertion, or b) dissable this particular assertion.

Tried:

  • Using update() instead of repaint(). This doesn't cause an assertion, but it also doesn't repaint because the main thread is too busy loading in the shared libraries, etc, and the timer events don't get processed until I'm ready to call finish on the splash screen.
  • starting QTimer in main loop. Same result as above.
  • Using a QT::QueuedConnection. Same result.

Main

#include <QApplication>
#include <QtGui>
#include <QTimer>
#include <QThread>

#include "mySplashScreen.h"
#include "myMainWindow.h"       // contains a configure function which takes a
                                // LONG time to load.

int main( int argc, char* argv[] )
{
    // estimate the load time
    int previousLoadTime_ms = 10000;

    QApplication a(argc, argv);
    MySplashScreen* splash = new MySplashScreen(QPixmap(":/splashScreen"));

    // progress timer. Has to be in a different thread since the
    // qApplication isn't started.
    QThread* timerThread = new QThread;

    QTimer* timer = new QTimer(0); // _not_ this!
    timer->setInterval(previousLoadTime_ms / 100.0);
    timer->moveToThread(timerThread);

    QObject::connect(timer, &QTimer::timeout, [&]
    {
        qApp->processEvents(); splash->incrementProgress(1); 
    });
    QObject::connect(timerThread, SIGNAL(started()), timer, SLOT(start()));
    timerThread->start();

    splash->show();
    a.processEvents();

    myMainWindow w;

    QTimer::singleShot(0, [&]
    {
        // This will be called as soon as the exec() loop starts.
        w.configure();  // this is a really slow initialization function
        w.show();
        splash->finish(&w);

        timerThread->quit();
    });

    return a.exec();
}

Splash Screen

#include <QSplashScreen>

class MySplashScreen : public QSplashScreen
{ 
    Q_OBJECT

public:

    MySplashScreen(const QPixmap& pixmap = QPixmap(), Qt::WindowFlags f = 0) 
        : QSplashScreen(pixmap, f)
    {
        m_pixmap = pixmap;
    }

    virtual void drawContents(QPainter *painter) override
    {
        QSplashScreen::drawContents(painter);

        // draw progress bar
    }

public slots:

    virtual void incrementProgress(int percentage)
    {
        m_progress += percentage;

        repaint();
    }

protected:

    int m_progress = 0;

private:

    QPixmap m_pixmap;
};

MyMainWindow

#include <QMainWindow>

class myMainWindow : public QMainWindow
{
public:

    void configure()
    {
        // Create and configure a bunch of widgets. 
        // This takes a long time.
    }
}
Nicolas Holthaus
  • 7,763
  • 4
  • 42
  • 97

1 Answers1

1

The problems are because the design is backwards. The GUI thread should not be doing any loading. The general approach to GUI threads is: do no work in the GUI thread. You should spawn a worker thread to load what you need loaded. It can post events (or invoke slots using a queued connection) to the GUI thread and its splash screen.

Of course, the worker thread should not create any GUI objects - it can't instantiate anything deriving from QWidget. It can, though, instantiate other things, so if you need any expensive-to-obtain data, prepare it in the worker thread, and then cheaply build a QWidget in the GUI thread once that data is available.

If your delays are due to library loading, then do load all the libraries in a worker thread, explicitly, and ensure that all of their pages are resident in memory - for example by reading the entire .DLL after you're loaded it as a library.

The MyMainWindow::configure() could be called in a worker thread, as long as it doesn't invoke any QWidget methods nor constructors. It can do GUI work, just not visible on screen. For example, you can load QImage instances from disk, or do painting on QImages.

This answer provides several approaches to executing a functor in a different thread, GCD-style.

If you are constructing widgets that are expensive to construct, or construct many of them, it's possible to make sure that the event loop can run between the instantiation of each widget. For example:

class MainWindow : public QMainWindow {
  Q_OBJECT
  QTimer m_configureTimer;
  int m_configureState = 0;
  Q_SLOT void configure() {
    switch (m_configureState ++) {
    case 0:
      // instantiate one widget from library A
      break;
    case 1:
      // instantiate one widget from library B
      ...
      break;
    case 2:
      // instantiate more widgets from A and B
      ...
      break;
    default:
      m_configureTimer.stop();
      break;
    }
  }
public:
  MainWindow(QWidget * parent = 0) : QMainWindow(parent) {
    connect(&m_configureTimer, SIGNAL(timeout()), SLOT(configure()));
    m_configureTimer.start(0);
    //...
  }
};
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • "Of course, the worker thread should not create any GUI objects - it can't instantiate anything deriving from QWidget." - That's *all* that `configure` does. It just makes a ton of them, which takes forever especially in debug mode when all the shared libraries are loaded through visual studio. – Nicolas Holthaus Mar 06 '15 at 18:39
  • @NicolasHolthaus Do those widgets do any work in their constructors? Are those stock widgets, or widgets that you wrote yourself? One approach would be to maintain state in the widget, make `configure()` a slot, and tie it to a zero-expiration timer, effectively invoking it every time the event queue is empty. The `configure` would then build things one object at a time, always returning. Eventually, its work done, it could disconnect itself from the timer. – Kuba hasn't forgotten Monica Mar 06 '15 at 18:42
  • 1
    @NicolasHolthaus Another approach would be to statically link Qt and your application. You'd then have a single executable with no dependencies, and it'd load very quickly. – Kuba hasn't forgotten Monica Mar 06 '15 at 18:43
  • I like the stateful approach, and may refactor that way. I had something similar before, but in the end each 'increment' took very different amounts of time, and so the progress bar ended up being choppy and thus not that helpful at estimating time-to-go. My current approach, while flawed, generates a very smooth progress bar, which is what I was looking for. Getting the same look using sub-states may require a vast number of them. – Nicolas Holthaus Mar 06 '15 at 18:49
  • @NicolasHolthaus Your current approach has no chance of working. Ever. Not without writing platform-specific code, that is. You certainly *could* do widget painting and whatnot from a worker thread, but this would be Windows-only, and you'd need a mutex that ensures that the worker thread stops doing the updates at the point where `configure()` is done. You'd need to do some deep diving into Qt internals to see how it paints on the backing store, and how the backing store is synced. On Windows, the backing store is a QImage that maps a DIB section, so it could be sync'd from any thread. – Kuba hasn't forgotten Monica Mar 06 '15 at 18:53
  • @NicolasHolthaus You will need to assume some total startup time. Once `configure()` is first entered, you simply take the current time, and then push the progress bar according to how much time has passed out of the assumed time. That's what you're doing anyway, right? – Kuba hasn't forgotten Monica Mar 06 '15 at 18:57
  • @NicolasHolthaus A texture-backed OpenGL widget could also be updated from another thread without any trouble, as long as you properly move the GL context between threads. – Kuba hasn't forgotten Monica Mar 06 '15 at 18:58