Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Refreshing data using QTimer



  • Hi Qt Forum,

    Please can you help with the following queries about refreshing data. It's one of those topics which must be blindingly obvious when you've done it once, and a bit unclear when you try it for the first time.

    Intro
    Our GUI application lets users connect to a database by clicking on a button, after which various widgets are updated with data. After connecting, the application then needs to poll the database for new data at specific times of the day, and update the widgets (charts, labels, etc in the GUI) as appropriate. This needs to happen in a separate thread, so as not to block the GUI while the update is in progress.

    We got this working by subclassing QThread and putting an infinite loop inside QThread::run(), which constantly polls the time of day and emits a signal to update the interface at certain points in time. Constantly polling the time of day, even though it's happening in a separate thread, feels like an incredibly bad idea, but we're not sure how to correctly refactor this.

    Our questions

    1. What is the best practice in terms of Qt classes to execute timed events in a separate thread? Ie, does the timer go in the main application, and the updating happens in a separate thread? Or does a separate thread own the timer AND the functions which do the updating?
    2. When trying to do something at a specific time of day in Qt, must one always use QTimer with an interval? If I want something to happen at midnight, it feels a bit long-winded to have to calculate the time from now til midnight, and then set a timer to go off after that amount of time.
    3. If we want different elements of the GUI to update at different times of the day, should we set a separate QTimer for each of them?

    Resources we have found which we can't quite understand/don't fully answer the question
    https://doc.qt.io/qt-5/qtimer.html
    https://stackoverflow.com/questions/40336912/sending-signal-in-special-time-like-hhmm-with-qt
    https://stackoverflow.com/questions/22481959/how-to-emit-a-qt-signal-daily-at-a-given-time



  • There is some confusion about the correct way to farm jobs out to QThreads so I was hoping somebody would chip in with a bit more detail. You can either do it by subclassing QThread and doing the work in there, or by creating a QObject which does the work and moving it to a vanilla QThread (both ways are shown here).

    We've plumped for the QObject + vanilla QThread method, which has taken all of an hour and a half to program, so I guess it's not too bad even for developers who don't normally use QThread.

    Our working solution

    //MainGuiProgram.cpp
    
    MainGuiProgram::MainGuiProgram(QWidget *parent)
       : QMainWindow(parent)
    {
       ui.setupUi(this);
    
       RefreshDataWorker *worker = new RefreshDataWorker;//our worker class
       worker->moveToThread(&workerThread);//MainGuiProgram has a private member QThread workerThread
       connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
       connect(this, &CrawlerManager::triggerWorkerToWork, worker, &RefreshDataWorker::doWork);//MainGuiProgram has a signal which tells the worker class to start
       connect(worker, &RefreshDataWorker::resultReady, this, &CrawlerManager::updateResults);//Gets results back from the worker when it's finished
       workerThread.start();
    }
    
    void MainGuiProgram::connectToDatabase()
    {
       //connect to database here, this connection is used by the GUI
       QTimer *t = new QTimer(this);
       t->setInterval(1000);
       connect(t, SIGNAL(timeout()), this, SLOT(refreshGuiData()));
       t->start();
    }
    
    void MainGuiProgram::refreshGuiData()
    {
       QDateTime nextRefreshTime = QDateTime::fromString("20/07/2021 09:00", "dd/MM/yyyy HH:mm"); //hard coded for example's sake
       if (QDateTime::currentDateTimeUtc().secsTo(expiry) <= 0 && updateHasHappened == false)
       {
          ui.statusLabel->setText("Update in progress!");
          ui.statusLabel->repaint();
    
          emit triggerWorkerToWork();//The signal which tells the worker to do its work
    	  
          updateHasHappened = true;
       }
    }
    
    void MainGuiProgram::updateResults(const QString &result)
    {
       ui.statusLabel->setText(result);
    }
    
    //RefreshDataWorker.cpp
    
    void RefreshDataWorker::doWork()
    {
       //connect to database again here, this connection is used by the worker
       //and thrown away when the refresh is finished
       ourDatabaseClass.doStuffWithDatabase();
       QString result = "Done!";
       emit resultReady(result);//a signal which the Main GUI receives when the result is done
    }
    

    Passing the result back using a signal allows the Main GUI thread to update the user interface, thus avoiding @jsulm 's warning about not touching the GUI from other threads.

    One important point not shown in this code is that the database connections (MySQL) needed to be named, so that the worker can identify and delete its own connection without mucking up the connection used by the main thread.

    Solution to our original questions

    1. What is the best practice in terms of Qt classes to execute timed events in a separate thread? Create a QObject worker, farm it out to a basic QThread instance, and tell it when to work using signals triggered from a QTimer running in the program's main thread.
    2. When trying to do something at a specific time of day in Qt, must one always use QTimer with an interval? Yes, but you can tell the timer to sleep for periods of 1 second at a time in order to not have to constantly poll the system time to know when to start working.
    3. If we want different elements of the GUI to update at different times of the day, should we set a separate QTimer for each of them? No. You can use one QTimer, which wakes up every second, and then each time it wakes up inspect the current time to know which jobs to kick off

    If nobody posts any other comments with respect to the above I'll mark it as the solution later today. Happy days


  • Lifetime Qt Champion

    @Jez1337 Well, depending on how exact it has to be you could simply use a QTimer with, for example, 1sec timeout. Means: once per second you would check the time and if needed trigger the update. Checking time once per second is not problematic in terms of CPU usage. Whether you need a second thread then depends on how long it takes to fetch the data from the database. If it is fast you do not have to mess with threads at all.



  • Thanks! So, we do need to use QTimer, and poll for the time of day. It's just that polling once a second is not very CPU intensive, so it's ok.

    The refresh does take quite a lot of time, and so we do need it to execute in a new thread. After @jsulm 's advice, our code now looks like this - what is the correct way to push this work onto another thread?

    void MainGuiProgram::connectToDatabase()
    {
       //connect to database here
       QTimer *t = new QTimer(this);
       t->setInterval(1000);
       connect(t, SIGNAL(timeout()), this, SLOT(refreshGuiData()));
       t->start();
    }
    
    void MainGuiProgram::refreshGuiData()
    {
       QDateTime nextRefreshTime = QDateTime::fromString("20/07/2021 09:00", "dd/MM/yyyy HH:mm"); //hard coded for example's sake
       if (QDateTime::currentDateTimeUtc().secsTo(expiry) <= 0 && updateHasHappened == false)
       {
          ui.statusLabel->setText("Update in progress!");
          ui.statusLabel->repaint();
    
          ourDatabaseClass->doStuffWithDatabase();//this blocks the GUI thread for 10-60 seconds! Not ok!
    
           ui.statusLabel->setText("done!");
          updateHasHappened = true;
       }
    }
    

  • Lifetime Qt Champion

    @Jez1337 Check https://doc.qt.io/qt-5/thread-basics.html
    One important point: do not access UI from other threads! This is not supported.
    You can execute ourDatabaseClass->doStuffWithDatabase() in another thread (you will need to move ourDatabaseClass object to that other thread). And then emit signals to main thread to send results (as signal parameters) to main UI.



  • There is some confusion about the correct way to farm jobs out to QThreads so I was hoping somebody would chip in with a bit more detail. You can either do it by subclassing QThread and doing the work in there, or by creating a QObject which does the work and moving it to a vanilla QThread (both ways are shown here).

    We've plumped for the QObject + vanilla QThread method, which has taken all of an hour and a half to program, so I guess it's not too bad even for developers who don't normally use QThread.

    Our working solution

    //MainGuiProgram.cpp
    
    MainGuiProgram::MainGuiProgram(QWidget *parent)
       : QMainWindow(parent)
    {
       ui.setupUi(this);
    
       RefreshDataWorker *worker = new RefreshDataWorker;//our worker class
       worker->moveToThread(&workerThread);//MainGuiProgram has a private member QThread workerThread
       connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
       connect(this, &CrawlerManager::triggerWorkerToWork, worker, &RefreshDataWorker::doWork);//MainGuiProgram has a signal which tells the worker class to start
       connect(worker, &RefreshDataWorker::resultReady, this, &CrawlerManager::updateResults);//Gets results back from the worker when it's finished
       workerThread.start();
    }
    
    void MainGuiProgram::connectToDatabase()
    {
       //connect to database here, this connection is used by the GUI
       QTimer *t = new QTimer(this);
       t->setInterval(1000);
       connect(t, SIGNAL(timeout()), this, SLOT(refreshGuiData()));
       t->start();
    }
    
    void MainGuiProgram::refreshGuiData()
    {
       QDateTime nextRefreshTime = QDateTime::fromString("20/07/2021 09:00", "dd/MM/yyyy HH:mm"); //hard coded for example's sake
       if (QDateTime::currentDateTimeUtc().secsTo(expiry) <= 0 && updateHasHappened == false)
       {
          ui.statusLabel->setText("Update in progress!");
          ui.statusLabel->repaint();
    
          emit triggerWorkerToWork();//The signal which tells the worker to do its work
    	  
          updateHasHappened = true;
       }
    }
    
    void MainGuiProgram::updateResults(const QString &result)
    {
       ui.statusLabel->setText(result);
    }
    
    //RefreshDataWorker.cpp
    
    void RefreshDataWorker::doWork()
    {
       //connect to database again here, this connection is used by the worker
       //and thrown away when the refresh is finished
       ourDatabaseClass.doStuffWithDatabase();
       QString result = "Done!";
       emit resultReady(result);//a signal which the Main GUI receives when the result is done
    }
    

    Passing the result back using a signal allows the Main GUI thread to update the user interface, thus avoiding @jsulm 's warning about not touching the GUI from other threads.

    One important point not shown in this code is that the database connections (MySQL) needed to be named, so that the worker can identify and delete its own connection without mucking up the connection used by the main thread.

    Solution to our original questions

    1. What is the best practice in terms of Qt classes to execute timed events in a separate thread? Create a QObject worker, farm it out to a basic QThread instance, and tell it when to work using signals triggered from a QTimer running in the program's main thread.
    2. When trying to do something at a specific time of day in Qt, must one always use QTimer with an interval? Yes, but you can tell the timer to sleep for periods of 1 second at a time in order to not have to constantly poll the system time to know when to start working.
    3. If we want different elements of the GUI to update at different times of the day, should we set a separate QTimer for each of them? No. You can use one QTimer, which wakes up every second, and then each time it wakes up inspect the current time to know which jobs to kick off

    If nobody posts any other comments with respect to the above I'll mark it as the solution later today. Happy days


Log in to reply