从后台工作线程修改Qt GUI

28
我在Qt中工作,当我按下"GO"按钮时,需要不断向网络发送数据包并使用接收到的信息修改界面。
问题在于我在按钮中有一个`while(1)`循环,所以按钮永远不会停止,因此界面也永远不会更新。我考虑在按钮中创建一个线程,并将`while(){}`代码放在那里。
我的问题是如何从线程中修改界面?(例如,如何从线程中修改文本框?)
4个回答

52

Qt的重要之处在于你必须只能在GUI主线程中使用Qt GUI。

因此,正确的方法是从工作线程向主线程发送通知,而主线程中的代码将实际更新文本框、进度条或其他内容。

我认为最好的方法是使用QThread代替posix线程,并使用Qt 信号在线程之间进行通信。这将成为您的工作线程,一个thread_func的替代者:

class WorkerThread : public QThread {
    void run() {
        while(1) {
             // ... hard work
             // Now want to notify main thread:
             emit progressChanged("Some info");
        }
    }
    // Define signal:
    signals:
    void progressChanged(QString info);
};

在您的小部件中,使用与.h文件中信号相同原型定义一个插槽
class MyWidget : public QWidget {
    // Your gui code

    // Define slot:
    public slots:
    void onProgressChanged(QString info);
};

在.cpp文件中实现此函数:

void MyWidget::onProgressChanged(QString info) {
    // Processing code
    textBox->setText("Latest info: " + info);
}

现在,在您想要生成线程的位置(例如,在按钮点击时):
void MyWidget::startWorkInAThread() {
    // Create an instance of your woker
    WorkerThread *workerThread = new WorkerThread;
    // Connect our signal and slot
    connect(workerThread, SIGNAL(progressChanged(QString)),
                          SLOT(onProgressChanged(QString)));
    // Setup callback for cleanup when it finishes
    connect(workerThread, SIGNAL(finished()),
            workerThread, SLOT(deleteLater()));
    // Run, Forest, run!
    workerThread->start(); // This invokes WorkerThread::run in a new thread
}

在连接信号和槽之后,在工作线程中使用emit progressChanged(...)来发送消息到主线程,主线程将调用与该信号连接的槽,即这里的onProgressChanged


2
感谢这篇精彩的文章,它真的帮助我理解了很多QT中新的东西!但我仍然遇到了UI锁定的问题。第一个代码块应该放在.h文件还是.cpp文件中? - David
3
@David,实际上.h和.cpp文件扩展名并不重要。您确定您是使用workerThread->start()而不是使用workerThread->run()启动线程吗? - NIA
1
我明白了,谢谢!我改了一些东西,现在搞定了。非常感谢。 - David
此代码的用户请注意:该代码中的“Changed”拼写错误为“Chagned”。虽然一直如此,但请确保更改或继续颠倒n和g。 - user2503170
呃,谢谢 @user2503170 ,我已经修正了拼写错误。 - NIA

1
因此,机制是您不能在线程内部修改小部件,否则应用程序将崩溃并显示错误,例如:
QObject::connect: Cannot queue arguments of type 'QTextBlock'
(Make sure 'QTextBlock' is registered using qRegisterMetaType().)
QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)
Segmentation fault

为了解决这个问题,您需要将线程工作封装在一个类中,例如:

class RunThread:public QThread{
  Q_OBJECT
 public:
  void run();

 signals:
  void resultReady(QString Input);
};

run() 函数包含您想要执行的所有工作。

在您的父类中,您将拥有一个调用函数来生成数据和一个 QT 窗口小部件更新函数:

class DevTab:public QWidget{
public:
  void ThreadedRunCommand();
  void DisplayData(QString Input);
...
}

然后,要调用线程,您需要连接一些插槽,这些插槽...
void DevTab::ThreadedRunCommand(){
  RunThread *workerThread = new RunThread();
  connect(workerThread, &RunThread::resultReady, this, &DevTab::UpdateScreen);
  connect(workerThread, &RunThread::finished, workerThread, &QObject::deleteLater);
  workerThread->start();  
}

连接函数有4个参数,第1个参数是原因类,第2个参数是该类中的信号。参数3是回调函数的类,参数4是类内的回调函数。
然后您可以在子线程中编写一个函数来生成数据:
void RunThread::run(){
  QString Output="Hello world";
  while(1){
    emit resultReady(Output);
    sleep(5);
  }
}

然后你的父函数中会有一个回调函数来更新小部件:
void DevTab::UpdateScreen(QString Input){
  DevTab::OutputLogs->append(Input);
}

然后,当你运行它时,父级中的小部件将在每次线程中调用emit宏时更新。如果连接函数被正确配置,它将自动获取发出的参数,并将其存储到回调函数的输入参数中。
工作原理如下:
1. 我们初始化该类。 2. 我们设置处理线程完成后会发生什么以及如何处理“返回”的数据(也称为发出数据),因为我们无法以通常的方式从线程中返回数据。 3. 然后我们使用调用`->start()`(这是硬编码到QThread中的)来运行线程,并且QT会查找类中硬编码的名称`.run()`成员函数。 4. 每次在子线程中调用`emit resultReady`宏时,它都会将QString数据存储到一些共享数据区域中,该区域被卡在线程之间的某个地方。 5. QT检测到resultReady已触发,并向您的函数UpdateScreen(QString)发出信号,以接受从run()发出的QString作为父线程中的实际函数参数。 6. 这将在每次触发emit关键字时重复发生。
基本上,connect()函数是子线程和父线程之间的接口,以便数据可以来回传输。 注意:resultReady()不需要定义。将其视为QT内部存在的宏即可。

0

你可以使用invokeMethod()或信号和槽机制,基本上有很多示例,例如如何发射信号以及如何在SLOT中接收它。但是,InvokeMethod似乎更有趣。

以下是一个示例,它展示了如何从线程中更改标签的文本:

//file1.cpp

QObject *obj = NULL; //global 
QLabel *label = new QLabel("test");
obj = label;   //Keep this as global and assign this once in constructor.

接下来在你的WorkerThread中,你可以按照以下方式进行:

//file2.cpp(即线程)

extern QObject *obj;
void workerThread::run()
{
     for(int i = 0; i<10 ;i++
     {
         QMetaObject::invokeMethod(obj, "setText",
                                Q_ARG(QString,QString::number(i)));
     }
     emit finished();
}

你的解决方案不完整,但更重要的是,使用了一个可能未初始化(或无效)的全局指针。 - jonspaceharper
感谢 @JonHarper 指出。我已经更改了它。我认为任何人都可以理解这个简单的例子,所以我没有给出完整的例子。你在上面的代码中发现了任何与内存相关的问题吗?如果有,请发布它。 - pra7

-4

你可以通过传递指向线程函数的指针来启动线程(在 POSIX 中,线程函数的签名为void* (thread_func)(void*),在 Windows 下也是类似的)。你完全可以自由地发送指向自己数据的指针(结构体或其他类型),并从线程函数中使用它(将指针转换为正确的类型)。好吧,内存管理应该考虑周全(这样你既不会泄漏内存,也不会从线程中使用已经释放的内存),但这是另一个问题。


你似乎完全没有理解问题的要点。 - Dženan
我的问题是如何从线程中修改界面?(例如,我如何从线程中修改textBox?) - Dženan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接