Search code examples
qtqt5qthreadqt6

Qt: how to update a frontend GUI with a backend thread?


In Qt, how to update a frontend GUI with a backend thread?

This question may have been asked several times, but, couldn't find a top-down answer and simple-working example... Ending with nothing that works, and, misleading Qt docs all over the places that tells everything and it's opposite (override QThread::run in old days, don't do that but use "Workers" with movetoThread use workers, and now "prefer run, avoid workers"?! do not use workers). At the end of the day, basic need, headache without solution...

With this CMakeLists.txt:

>> cat CMakeLists.txt 
cmake_minimum_required(VERSION 3.0)
project(dummyGUI LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt5 COMPONENTS Core Widgets REQUIRED)
add_executable(dummyGUI "main.cpp" "QDummyGUI.hpp" "QDummyWorker.hpp")
target_include_directories(dummyGUI PUBLIC Qt5::Core Qt5::Widgets)
target_link_libraries(dummyGUI PUBLIC Qt5::Core Qt5::Widgets)

I create a Qt application:

>> cat main.cpp 
#include <QApplication>

#include "QDummyGUI.hpp"

int main(int argc, char **argv) {
  QApplication app(argc, argv);
  QDummyGUI gui;
  gui.show();
  return app.exec();
}

With a basic GUI which has just a counter to display:

>> cat QDummyGUI.hpp 
#pragma once
#include <QMainWindow>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>
#include <QThread>
#include "QDummyWorker.hpp"
#include <sstream>

class QDummyGUI : public QMainWindow {
  Q_OBJECT

  public:
    QDummyGUI(QWidget * parent = nullptr) : QMainWindow(parent) {
      // Build GUI.
      counter_ = new QLabel("0", this);
      auto * rootLayout_ = new QVBoxLayout();
      rootLayout_->addWidget(counter_);
      auto * window = new QWidget(this);
      window->setLayout(rootLayout_);
      setCentralWidget(window);
      // Runs task in backend with thread + worker.
      backendWorker_ = new QDummyWorker(); // do NOT set parent here.
      backendWorker_->moveToThread(&backendThread_);
      QObject::connect(&backendThread_, &QThread::started,
                       backendWorker_, &QDummyWorker::started);
      QObject::connect(&backendThread_, &QThread::finished,
                       backendWorker_, &QDummyWorker::finished);
      QObject::connect(backendWorker_, &QDummyWorker::sendData,
                       this, &QDummyGUI::recvData);
      backendThread_.start(); // Thread = worker parent HERE only.
    };
    ~QDummyGUI() {
      backendThread_.quit(); // Stop thread (send stop signal).
      backendThread_.wait(); // Wait for thread to be stopped.
    }

  public slots:
    void recvData(unsigned int const & count) {
      std::stringstream str; str << count;
      counter_->setText(str.str().c_str());
    };

  private:
    QLabel * counter_;
    QThread backendThread_; // Runs in GUI backend
    QDummyWorker * backendWorker_; // Handled by thread.
};


I need the counter to be updated (incremented) by a thread which must run in backend (the thread watch over backend stuffs, do some job, produce a result, and send that result back to the frontend GUI):

>> cat QDummyWorker.hpp 
#pragma once
#include <QObject>
#include <thread>
#include <iostream>

class QDummyWorker : public QObject {
  Q_OBJECT

  public:
    QDummyWorker() {
      run_ = true;
    };

  public slots:
    void started() {
      std::cout << "QDummyWorker::started" << std::endl;
      unsigned int count = 0;
      while (run_) {
        // Need to constantly watch over stuffs and do some tasks accordingly.
        std::this_thread::sleep_for(std::chrono::seconds(1)); // Doing stuffs takes some time.
        count++; // Doing stuffs produce a result.
        emit sendData(count); // Now need to update GUI with result produced in backend.
      };
    };
    void finished() {
      std::cout << "QDummyWorker::finished" << std::endl;
      run_ = false;
    };

  signals:
    void sendData(unsigned int const & count); // Qt MOC will provide this implementation.

  private:
    bool run_;
};

Note: replacing finished signals/slots by quit doesn't help.

Finally ending with a GUI displaying an updated counter (incremented every second) but can't stop the thread (closing GUI doesn't stop the thread - need to finish it with Ctrl-C)

>> ./dummyGUI
QDummyWorker::started
^C

Questions:

  • What is the proper way to stop the thread when the GUI is closed (proper way to handle thread lifecycle)?
  • Is this the "good" way to go? What would be the recommended solution in this situation according to current Qt best practices (seems they evolved...)?
  • The "thread job" is actually an image conversion (which can take some time), and, the "counter" count is actually an image (so it's potentially a big data to transfer from backend thread to frontend GUI - large memory footprint): is there a way to make data (= count in this dummy example) copies will not occur when calling sendData and recvData?

Solution

  • Best solution found:

    >> cat QDummyGUI.hpp 
    #pragma once
    #include <QMainWindow>
    #include <QLabel>
    #include <QVBoxLayout>
    #include <QWidget>
    #include <QThread>
    #include "QDummyWorker.hpp"
    #include <sstream>
    
    class QDummyGUI : public QMainWindow {
      Q_OBJECT
    
      public:
        QDummyGUI(QWidget * parent = nullptr) : QMainWindow(parent) {
          // Build GUI.
          counter_ = new QLabel("0", this);
          auto * rootLayout_ = new QVBoxLayout();
          rootLayout_->addWidget(counter_);
          auto * window = new QWidget(this);
          window->setLayout(rootLayout_);
          setCentralWidget(window);
          // Runs task in backend with thread + worker.
          backendWorker_ = new QDummyWorker(); // do NOT set parent here.
          backendWorker_->moveToThread(&backendThread_);
          QObject::connect(&backendThread_, &QThread::started,
                           backendWorker_, &QDummyWorker::started);
          QObject::connect(&backendThread_, &QThread::finished,
                           backendWorker_, &QObject::deleteLater);
          QObject::connect(backendWorker_, &QDummyWorker::sendData,
                           this, &QDummyGUI::recvData);
          backendThread_.start(); // Thread = worker parent HERE only.
        };
        ~QDummyGUI() {
          backendThread_.requestInterruption(); // Stop Worker.
          backendThread_.quit(); // Stop thread (send stop signal).
          backendThread_.wait(); // Wait for thread to be stopped.
        }
    
      public slots:
        void recvData(unsigned int const & count) {
          std::stringstream str; str << count;
          counter_->setText(str.str().c_str());
        };
    
      private:
        QLabel * counter_;
        QThread backendThread_; // Runs in GUI backend
        QDummyWorker * backendWorker_; // Handled by thread.
    };
    
    
    
    >> cat QDummyWorker.hpp 
    #pragma once
    #include <QObject>
    #include <thread>
    #include <iostream>
    
    class QDummyWorker : public QObject {
      Q_OBJECT
    
      public:
        QDummyWorker() {
          std::cout << "QDummyWorker::created" << std::endl;
        };
        ~QDummyWorker() {
          std::cout << "QDummyWorker::destroyed" << std::endl;
        };
    
      public slots:
        void started() {
          std::cout << "QDummyWorker::started" << std::endl;
          unsigned int count = 0;
          while (!QThread::currentThread()->isInterruptionRequested()) {
            // Need to constantly watch over stuffs and do some tasks accordingly.
            std::this_thread::sleep_for(std::chrono::seconds(1)); // Doing stuffs takes some time.
            count++; // Doing stuffs produce a result.
            emit sendData(count); // Now need to update GUI with result produced in backend.
          };
        };
    
      signals:
        void sendData(unsigned int const & count); // Qt MOC will provide this implementation.
    };