如何使用后端线程更新前端 GUI?

huangapple go评论74阅读模式
英文:

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

问题

在Qt中,如何使用后台线程更新前端GUI?

这个问题可能已经被问过多次了,但是找不到一个从上到下的答案和一个简单可行的示例... 最终什么都没有找到,还有误导性的Qt文档到处都是,告诉了所有东西和它的相反(在旧时代覆盖QThread::run,现在不要那样做,而要使用“工作线程”与movetoThread 使用工作线程,而现在“更喜欢run,避免使用工作线程”?! 不要使用工作线程)。最终,基本需求,却头痛欲裂没有解决方案...

使用这个 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)

我创建了一个Qt应用程序:

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();
}

带有一个简单GUI的基本界面,其中只有一个计数器用于显示:

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) {
      // 构建GUI。
      counter_ = new QLabel("0", this);
      auto * rootLayout_ = new QVBoxLayout();
      rootLayout_->addWidget(counter_);
      auto * window = new QWidget(this);
      window->setLayout(rootLayout_);
      setCentralWidget(window);
      // 使用线程 + 工作线程在后台运行任务。
      backendWorker_ = new QDummyWorker(); // 不要在这里设置父对象。
      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(); // 只有在这里将线程设置为工作线程的父对象。
    };
    ~QDummyGUI() {
      backendThread_.quit(); // 停止线程(发送停止信号)。
      backendThread_.wait(); // 等待线程停止。
    }

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

  private:
    QLabel * counter_;
    QThread backendThread_; // 在GUI后台运行
    QDummyWorker * backendWorker_; // 由线程处理。
};

我需要通过一个后台线程更新(递增)计数器,该线程必须在后台运行(线程监视后台工作,执行一些工作,生成一个结果,并将该结果发送回前端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_) {
        // 需要不断监视并根据情况执行一些任务。
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 做一些事情需要一些时间。
        count++; // 做一些事情会产生一个结果。
        emit sendData(count); // 现在需要使用在后台生成的结果更新GUI。
      };
    };
    void finished() {
      std::cout << "QDummyWorker::finished" << std::endl;
      run_ = false;
    };

  signals:
    void sendData(unsigned int const & count); // Qt MOC将提供此实现。

  private:
    bool run_;
};

注意:用finished信号/槽替换quit并不能解决问题。

最终得到一个显示更新的计数器(每秒递增一次)的GUI,但无法停止线程(关闭GUI不会停止线程 - 需要使用Ctrl-C来结束它)。

问题:

  • 当关闭GUI时,如何正确地停止线程(如何处理线程的生命周期)?
  • 这是一个“好”的方式吗?根据当前的Qt最佳实践,推荐在这种情况下使用什么解决方案(似乎它们发生了变化...)?
  • “线程工作”实际上是图像转换(可能需要一些时间),而“计数器”count实际上是一个图像(因此从后台线程传递到前端GUI的数据可能是一个数据 - 内存占用很大):有没有办法在调用sendDatarecvData时不产生数据(在这个虚拟示例中是count)的副本?
英文:

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:

&gt;&gt; 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 &quot;main.cpp&quot; &quot;QDummyGUI.hpp&quot; &quot;QDummyWorker.hpp&quot;)
target_include_directories(dummyGUI PUBLIC Qt5::Core Qt5::Widgets)
target_link_libraries(dummyGUI PUBLIC Qt5::Core Qt5::Widgets)

I create a Qt application:

&gt;&gt; cat main.cpp 
#include &lt;QApplication&gt;

#include &quot;QDummyGUI.hpp&quot;

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:

&gt;&gt; cat QDummyGUI.hpp 
#pragma once
#include &lt;QMainWindow&gt;
#include &lt;QLabel&gt;
#include &lt;QVBoxLayout&gt;
#include &lt;QWidget&gt;
#include &lt;QThread&gt;
#include &quot;QDummyWorker.hpp&quot;
#include &lt;sstream&gt;

class QDummyGUI : public QMainWindow {
  Q_OBJECT

  public:
    QDummyGUI(QWidget * parent = nullptr) : QMainWindow(parent) {
      // Build GUI.
      counter_ = new QLabel(&quot;0&quot;, this);
      auto * rootLayout_ = new QVBoxLayout();
      rootLayout_-&gt;addWidget(counter_);
      auto * window = new QWidget(this);
      window-&gt;setLayout(rootLayout_);
      setCentralWidget(window);
      // Runs task in backend with thread + worker.
      backendWorker_ = new QDummyWorker(); // do NOT set parent here.
      backendWorker_-&gt;moveToThread(&amp;backendThread_);
      QObject::connect(&amp;backendThread_, &amp;QThread::started,
                       backendWorker_, &amp;QDummyWorker::started);
      QObject::connect(&amp;backendThread_, &amp;QThread::finished,
                       backendWorker_, &amp;QDummyWorker::finished);
      QObject::connect(backendWorker_, &amp;QDummyWorker::sendData,
                       this, &amp;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 &amp; count) {
      std::stringstream str; str &lt;&lt; count;
      counter_-&gt;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):

&gt;&gt; cat QDummyWorker.hpp 
#pragma once
#include &lt;QObject&gt;
#include &lt;thread&gt;
#include &lt;iostream&gt;

class QDummyWorker : public QObject {
  Q_OBJECT

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

  public slots:
    void started() {
      std::cout &lt;&lt; &quot;QDummyWorker::started&quot; &lt;&lt; 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 &lt;&lt; &quot;QDummyWorker::finished&quot; &lt;&lt; std::endl;
      run_ = false;
    };

  signals:
    void sendData(unsigned int const &amp; 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)

&gt;&gt; ./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?

答案1

得分: 0

以下是您要的中文翻译:

最佳解决方案:

#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) {
      // 构建 GUI。
      counter_ = new QLabel("0", this);
      auto * rootLayout_ = new QVBoxLayout();
      rootLayout_->addWidget(counter_);
      auto * window = new QWidget(this);
      window->setLayout(rootLayout_);
      setCentralWidget(window);
      // 使用线程 + 工作器在后台运行任务。
      backendWorker_ = new QDummyWorker(); // 不要在这里设置父对象。
      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(); // 线程 = 工作器的父对象只在这里设置。
    };
    ~QDummyGUI() {
      backendThread_.requestInterruption(); // 停止工作器。
      backendThread_.quit(); // 停止线程(发送停止信号)。
      backendThread_.wait(); // 等待线程停止。
    }

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

  private:
    QLabel * counter_;
    QThread backendThread_; // 在 GUI 后台运行
    QDummyWorker * backendWorker_; // 由线程处理。
};
#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()) {
        // 需要不断监视任务并相应地执行一些任务。
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 执行任务需要一些时间。
        count++; // 执行任务会产生结果。
        emit sendData(count); // 现在需要使用在后台生成的结果更新 GUI。
      };
    };

  signals:
    void sendData(unsigned int const & count); // Qt MOC 将提供此实现。
};
英文:

Best solution found:

&gt;&gt; cat QDummyGUI.hpp 
#pragma once
#include &lt;QMainWindow&gt;
#include &lt;QLabel&gt;
#include &lt;QVBoxLayout&gt;
#include &lt;QWidget&gt;
#include &lt;QThread&gt;
#include &quot;QDummyWorker.hpp&quot;
#include &lt;sstream&gt;

class QDummyGUI : public QMainWindow {
  Q_OBJECT

  public:
    QDummyGUI(QWidget * parent = nullptr) : QMainWindow(parent) {
      // Build GUI.
      counter_ = new QLabel(&quot;0&quot;, this);
      auto * rootLayout_ = new QVBoxLayout();
      rootLayout_-&gt;addWidget(counter_);
      auto * window = new QWidget(this);
      window-&gt;setLayout(rootLayout_);
      setCentralWidget(window);
      // Runs task in backend with thread + worker.
      backendWorker_ = new QDummyWorker(); // do NOT set parent here.
      backendWorker_-&gt;moveToThread(&amp;backendThread_);
      QObject::connect(&amp;backendThread_, &amp;QThread::started,
                       backendWorker_, &amp;QDummyWorker::started);
      QObject::connect(&amp;backendThread_, &amp;QThread::finished,
                       backendWorker_, &amp;QObject::deleteLater);
      QObject::connect(backendWorker_, &amp;QDummyWorker::sendData,
                       this, &amp;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 &amp; count) {
      std::stringstream str; str &lt;&lt; count;
      counter_-&gt;setText(str.str().c_str());
    };

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


&gt;&gt; cat QDummyWorker.hpp 
#pragma once
#include &lt;QObject&gt;
#include &lt;thread&gt;
#include &lt;iostream&gt;

class QDummyWorker : public QObject {
  Q_OBJECT

  public:
    QDummyWorker() {
      std::cout &lt;&lt; &quot;QDummyWorker::created&quot; &lt;&lt; std::endl;
    };
    ~QDummyWorker() {
      std::cout &lt;&lt; &quot;QDummyWorker::destroyed&quot; &lt;&lt; std::endl;
    };

  public slots:
    void started() {
      std::cout &lt;&lt; &quot;QDummyWorker::started&quot; &lt;&lt; std::endl;
      unsigned int count = 0;
      while (!QThread::currentThread()-&gt;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 &amp; count); // Qt MOC will provide this implementation.
};


huangapple
  • 本文由 发表于 2023年2月14日 01:04:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/75439026.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定