1

I created an application in which I would like to add a loading screen when the application is opening a project because the loading of the project can be long and sometimes, the gui blocks so the user can think there is a crash.

So I tried with QThread, reading doc and "solved" examples on this forum but nothing to do, I can't make it work.

I have a MainWindow class which deals with GUI and this class is the one I create in the main function :

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

Then I have : mainwindow.h

class MyThread;
class MainWindow : public QMainWindow
{
    Q_OBJECT
    ...
    private :
        Controller *controller;//inherits from QObject and loads the project
        QList<MyThread*> threads;
    public slots :
        void animateLoadingScreen(int inValue);
}

mainwindow.cpp

MainWindow::MainWindow(...)
{
    controller=new Controller(...);

    threads.append(new MyThread(30, this));
    connect(threads[0], SIGNAL(valueChanged(int)), this, SLOT(animateLoadingScreen(int)));
}

void MainWindow::animateLoadingScreen(int inValue)
{
    cout<<"MainWindow::animateLoadingScreen"<<endl;
    widgetLoadingScreen->updateValue(inValue);//to update the animation
}

void MainWindow::openProject()
{
    widgetLoadingScreen->show()://a widget containing a spinner for example
    threads[0]->start();//I want to launch the other thread here

    controller->openProject();

    threads[0]->exit(0);//end of thread so end of loading screen
}

MyThread.h

class MyThread : public QThread
{
    Q_OBJECT;
public:
    explicit MyThread(int interval, QObject* parent = 0);
    ~MyThread();

signals:
    void valueChanged(int);

private slots:
    void count(void);

protected:
    void run(void);

private:
    int i;
    int inc;
    int intvl;
    QTimer* timer;
};

MyThread.cpp

MyThread::MyThread(int interval, QObject* parent): QThread(parent), i(0), inc(-1), intvl(interval), timer(0)
{
}

void MyThread::run(void)
{
    if(timer == 0)
    {
        timer = new QTimer(this);
        connect(timer, SIGNAL(timeout()), this, SLOT(count()));
    }

    timer->start(intvl);
    exec();
}

void MyThread::count(void)
{
    if(i >= 100 || i <= 0)
        inc = -inc;
    i += inc;
    emit valueChanged(i);
}

When I execute the app, and click on open button which launches MainWindow::openProject(), I get :

QObject: Cannot create children for a parent that is in a different thread.
(Parent is MyThread(0x5463930), parent's thread is QThread(0x3cd1f80), current thread is MyThread(0x5463930)
MainWindow::animateLoadingScreen
MainWindow::animateLoadingScreen
....
MainWindow::animateLoadingScreen
MainWindow::animateLoadingScreen

(and here the controller outputs. and no MainWindow::animateLoadingScreen anymore so the widget loading screen is never animated during the opening of the project)

So what do I have to do, what do I have to put in MyThread class, how to link its signal to MainWindow to update the loading screen. And I think that there may be a problem with widgetLoadingScreen which is created in MainWindow so if MainWindow is blocked beacause of the opening, the widgetLoadingScreen can't be updated since it is in the thread of MainWindow which handles the GUI ?!

I read : http://www.qtcentre.org/wiki/index.php?title=Updating_GUI_from_QThread but with that one, I got error message at runtime, it's the one I use in the code I give above QObject: Cannot create children for a parent that is in a different thread. (Parent is MyThread(0x41938e0), parent's thread is QThread(0x1221f80), current thread is MyThread(0x41938e0)

I tried that too : How to emit cross-thread signal in Qt? but, even if I don't have the error message at runtime, it's the same for the animation which is not updated.

I am completely lost, and I don't think it's something difficult to do to have a loading screen in a thread while the main thread is opening a project?!

Community
  • 1
  • 1
SteveTJS
  • 635
  • 17
  • 32
  • Your thread doesn't even do anything. You might as well just be using a `QTimer` without the new thread. The heavy processing is what you need to move to a new thread, which to me seems to be in your `Controller` class. – thuga Jun 16 '14 at 09:07
  • 1
    This is another ***[I subclassed Qthread to do multithreading and it doesnt work](http://codethis.wordpress.com/2011/04/04/using-qthread-without-subclassing/)*** question – UmNyobe Jun 16 '14 at 09:09
  • @thuga From what I understood in http://www.qtcentre.org/wiki/index.php?title=Updating_GUI_from_QThread, it's the timer which makes that the run function executes the count function which emits the signal. I can't move the heavy process to a new thread, it will be even more complicated because the loading process emits several signals, ... to update other gui classes. That's why I would like to create a simple loading widget in another thread that I can run when I want for long loadingprocess. – SteveTJS Jun 16 '14 at 09:37
  • @SteveTJS Thuga is right. Qthread allow you to execute **slots** of object which **have been moved to a Qthread object**. so it's `controller->openProject();` which should be moved to aQthread and triggered by a slot. Qtcentre is not the official documentation, and I suspect that post to be old. [The qt doc shows how things are done](http://qt-project.org/doc/qt-4.8/qthread.html#details) – UmNyobe Jun 16 '14 at 09:45
  • 1
    @SteveTJS It's the heavy processing that's causing your GUI to freeze. It doesn't matter in what thread your `QTimer` object lives in, as the signals won't be processed in your main thread, until the heavy processing stops. – thuga Jun 16 '14 at 09:46
  • 1
    @SteveTJS If your `Controller` class is a simple `QObject` subclass, then you can easily move it to a new thread. Emitting signals will cause no problems, as long as you didn't connect them with the `Qt::DirectConnection` connection type. – thuga Jun 16 '14 at 10:16
  • thanks for your answers, I'll try all that and tell you if I succeed. – SteveTJS Jun 16 '14 at 12:58
  • "the loading process emits several signals, ... to update other gui classes" But that's **stupendous** and desirable! Those signals, upon emission, will result in `QMetaCallEvent` instances being posted to your GUI classes. The GUI classes will pick those up and "reconstitute" the intended slot calls. That's precisely what you want, and how, ideally, you might separate a worker thread from the GUI thread. The interface should be specified *entirely* in terms of signals and slots, with the worker not even having a pointer to the GUI instances (to avoid wrongly calling the slots directly). – Kuba hasn't forgotten Monica Jun 16 '14 at 15:49
  • so finally I just sent a signal from my Controller working function to update a progress bar in the main thread, and I did it as often as possible not to have this freezing effect and not too much not to slow down the process. – SteveTJS Aug 03 '14 at 10:38

3 Answers3

0

Qt has a QSplashScreen class which you could use for a loading screen.

Besides this, one of the problems you have is due to thread affinity (the thread in which an object is running) and the fact that you've decided to inherit from QThread.

In my opinion, QThread has a misleading name. It's more of a thread controller and unless you want to change how Qt handles threads, you really shouldn't inherit from it and many will tell you that "You're doing it Wrong!"

By inheriting QThread, your instance of MyThread is running on the main thread. However, when a call to exec() is made, it starts the event loop for the new thread and all its children, which includes the QTimer, and tries to move it to the new thread.

The correct way to solve this is not to inherit from QThread, but to create a worker object, derived from QObject and move it to the new thread. You can read about how to 'Really Truly Use QThread' here.

TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85
  • ok, I'll read that! I firstly tried with splashscreen but to animate it, I need another thread too. – SteveTJS Jun 16 '14 at 09:42
  • You don't need another thread if you call qApp.processEvents() every once in a while and update the screen with setPixamp – TheDarkKnight Jun 16 '14 at 09:45
  • The problem if I don't use another thread is that when the loading process blocks or takes (very) long time (load big images, create thumbnails,...), the loading screen won't be updated since the processEvents() is not executed since the process is blocks or takes .... :) loop again. That's why I would like to put that loading screen in another thread which won't be blocked by the loading process. – SteveTJS Jun 16 '14 at 09:58
  • You would call processEvents once in a while from within the Controller class. As you've not provided this code, I'm assuming it loads multiple files in succession. In this case, after each file load, call processEvents to allow the event loop to run before loading the next file. If your animation is just a percentage of progress this will work. However, if you want a smooth, continuous animation, then yes, you would require a separate thread. – TheDarkKnight Jun 16 '14 at 10:04
  • 3
    Calling `QApplication::processEvents` can be dangerous, so be careful with that. For example, imagine a situation where processed events cause your object from which you call `QApplication::processEvents` to be deleted. The result of this can cause your app to crash. – thuga Jun 16 '14 at 10:06
  • @thuga, I agree, which is why the answer describes how to use QThread by using a separate worker object, inherited from QObject. This is, of-course preferred over processEvents. – TheDarkKnight Jun 16 '14 at 10:11
0

1st problem - runtime error with QTimer

Problem is in void MyThread::run(void), line timer = new QTimer(this);. The thread instance is created in (and so owned by) different (main) thread then it represents. For this reason, you cannot use thread as parent for object created inside the thread. In your case, solution is simple - create the timer on stack.

Example:

void MyThread::run(void)
{
    QTimer timer;
    connect(&timer, SIGNAL(timeout()), this, SLOT(count()));
    timer.start(intvl);

    // Following method start event loop which makes sure that timer
    // will exists. When this method ends, timer will be automatically
    // destroyed and thread ends.
    exec();
}

2nd problem - GUI not updating

Qt requires the GUI to be created and displayed from main application thread. This means that blocking main thread with big operation blocks whole GUI. There are only 2 possible solutions:

  1. Move the heavy work to other (loading) thread. This is best solution and I believe it's worth the effort (I'm aware you wrote that this would be problematic).
  2. During the heavy operation, call regularly QCoreApplication::processEvents() - each time you call this, events will be processed, which includes calling slots, key and mouse events, GUI redraw etc. The more often you call this, the more responsive GUI will be. This is simpler to implement (just put one line all over the code), but it's quite ugly and if the operation blocks on loading a file, the GUI will freeze for some time. That means that responsiveness of your GUI depends on interval between calls. (But if you call it too often, you will slow down the loading progress.) This technique is discouraged and it's best to avoid it...
  • 1
    The timer will start in the calling thread. So this is as if the timer was created in mainwindow, only that it add the possibility to access an invalid object . – UmNyobe Jun 16 '14 at 09:14
  • As amazing it can seems `Qthread::run()` doesnt execute in a different thread. It is designed to launch an even loop (inside `exec()`) which use a different thread to execute ***slots***. You are correct on the thread affinity. But there is zero benefit of having that timer inside MyThread::run than outside. – UmNyobe Jun 16 '14 at 09:32
  • Replace `QTimer timer;` with `QPointer timer = new QTimer(); timer->moveToThread(this);` – Dmitry Sazonov Jun 16 '14 at 09:33
  • `timeout()` signal will be handled by the main thread. if someone call sender in `count()` and that for some reason the thread was stopped, timer is already destroyed. You need an unfortunate turn of events to get there, but you know... multithreading... – UmNyobe Jun 16 '14 at 09:34
  • Actually the timer is not invalid. `count` is executed in Mythread. The rest still holds though. – UmNyobe Jun 16 '14 at 09:41
  • @UmNyobe Yes it does. `QThread::run` runs in a new thread. This is why he gets the message `Cannot create children for a parent that is in a different thread`. – thuga Jun 16 '14 at 09:45
  • @UmNyobe "As amazing it can seems `Qthread::run()` doesnt execute in a different thread." Wait what? If that's the case, then I completely absolutely misunderstood the Qthread class documentation (and I'm wondering how my applications works as I make them on wrong assumptions), but thanks for clarification, in that case whole answer is wrong. About having QTimer outside - yes, it can be part of the `MyThread` class, what I wrote is just one of many possibilities. Another is, as Dmitry points out, to use some smart pointer, although I would use `QSharedPointer`. –  Jun 16 '14 at 09:47
  • @DmitrySazonov Yeah, that's another possibility, although AFAIK `moveToThread()` should be avoided. I would use `QSharedPointer` instead. –  Jun 16 '14 at 09:49
  • So I tried : void MyThread::run(void) { //QTimer timer; //or QPointer timer = new QTimer(); timer->moveToThread(this); connect(timer, SIGNAL(timeout()), this, SLOT(count())); timer->start(intvl); exec(); } But no one works unfortunately. – SteveTJS Jun 16 '14 at 09:54
  • @SteveTJS What's the error now? Still the same? Or compiler error? –  Jun 16 '14 at 09:55
  • @Laethnes You are correct that sentence is crap. What I meant is that the timer will not act as you expect. – UmNyobe Jun 16 '14 at 09:56
  • @SteveTJS Btw. note that if you used my version, you need to change `connect(timer, ...` to `connect(&timer...` and `timer->start` to `timer.start`. –  Jun 16 '14 at 09:57
  • @UmNyobe I see, ok, thanks. Well, it's about year I worked with `QTimer` last time... –  Jun 16 '14 at 09:57
  • @SteveTJS And if you used `QPointer`/`QSharedPointer` version, I don't think `connect(timer, ...` would work either. You can use `connect(timer.data(), ...`. (This is little problem with smart pointers.) –  Jun 16 '14 at 10:02
  • Yes thanks, I don't have any compile error, it's just that at runtime I still have the same thing: MainWindow::animateLoadingScreen MainWindow::animateLoadingScreen...MainWindow::animateLoadingScreen(the controller outputs. and no MainWindow::animateLoadingScreen anymore so the widget loading screen is never animated during the opening of the project) – SteveTJS Jun 16 '14 at 10:04
  • @SteveTJS Ouch, until now I didn't realize one big problem you have. Because it's longer to explain, so I added it to my answer. In short - you're right, your loading process is blocking GUI. –  Jun 16 '14 at 10:16
  • thanks for your answers, I'll try all that and tell you if I succeed. – SteveTJS Jun 16 '14 at 12:57
  • It's not that one should discourage calling `processEvents`. I'd consider its presence in one's code simply a bug. That method should not even exist. `processEvents` will gladly recurse into your code and makes it rather hard to reason about correctness of the code. It can "break" completely unrelated code running in the same thread. – Kuba hasn't forgotten Monica Jun 16 '14 at 15:57
0

The way I will do it based on the default example on Qthread Page:

  1. Break the big controller->openProject(); such that it updates a variable
  2. provide access to that variable using a signal ( like statusChanged(int)).
  3. Do what you need to do once the signal is triggered.

The code looks like

class Worker : public QObject
 {
     Q_OBJECT
     QThread workerThread;

 private: 
    int status; //
    bool hasworktoDo();
    void doSmallWork();

 public slots:
     void doWork() {

         while(hasworktoDo())
         {
             doSmallWork(); // fragments of controller->openProject();
             ++status; //previously set to 0
             emit statusChanged(status);
         }
         emit resultReady(result);
     }


 signals:
     void resultReady(const QString &result);
     void statusChanged(int value);
 };

 class Controller : public QObject
 {
     Q_OBJECT
     QThread workerThread;
 public:
     Controller() {
         Worker *worker = new Worker;
         worker->moveToThread(&workerThread);
         connect(workerThread, SIGNAL(statusChanged(int)), this, SLOT(updateStatus(int)));
         connect(workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()));
         connect(this, SIGNAL(operate()), worker, SLOT(doWork()));
         connect(worker, SIGNAL(resultReady(QString)), this, SLOT(handleResults(QString)));
         workerThread.start();
     }
     ~Controller() {
         workerThread.quit();
         workerThread.wait();
     }
 public slots:
     void handleResults(const QString &);

     void updateStatus(int value)
     {
       int status = value;
       //now you have what to use to update the ui
     }
 signals:
     void operate(); //
 };

If you have trouble breaking the openProject operations in small parts, it just mean it is pointless to say on the UI that you are at 50%... Just say Loading... and make the code simpler. Note that you don't need a timer in my case, as the worker sends regular updates

UmNyobe
  • 22,539
  • 9
  • 61
  • 90