在编写界面程序时,通常会运行一些代码执行比较耗时的任务。如果想在执行任务的过程中向用户提示当前处理进度,通常会在界面上增加一个进度条。如果这个任务运行在主程序的线程中,就会造成主界面的卡死,进度条根本无法实时更新,直到任务执行完界面才能恢复。这个时候就应该利用多线程技术,将耗时任务的代码放到一个新的线程中运行,同时向主程序线程更新进度。
接下来我用Qt中QThread来实现这个目的。
我用的编程环境是Visual Studio 2019 Community和Qt 5.12。
在这里我设计一个按钮来启动一个耗时任务,并在主界面上实时刷新进度条以显示任务运行的进度。
首先,创建一个带界面的Qt桌面程序工程,选【Qt Widgets Application】。我将工程重新命名为QtProgressTest。用Qt Designer打开.ui文件,在主界面上增加一个Push Button和Progress Bar控件,另外增加一个Label控件以显示一些提示文字。如下图。
接下来添加这个PushButton按钮的响应函数。在主程序增加一个void doSomething()槽(slot)函数,并在主类的构造函数中用connect语句将按钮的click信号与之相连。这个doSomething函数用于启动耗时任务。
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(doSomething()));
再增加一个槽函数void updateProgress(int p, const char* msg);用于刷新进度条。
void QtProgressTest::updateProgress(int p, const char* msg)
{
ui.progressBar->setValue(p);
ui.label->setText(msg);
}
接下来,在doSomething函数中想办法启动一个新线程用于执行耗时任务,并用updateProgress槽函数接收新线程的进度信号。
QThread多线程可以用两种方法。第一种是继承QThread类,将耗时任务代码编写在run函数中。第二种是继承QObject类,执行耗时任务,并用moveToThread函数。在这里我将两种方法都实现一下。
基本思路是继承QThread,重写run函数
创建QThread的派生类MyThread,增加一个成员变量,是主程序类对象的指针
QtProgressTest* m_thread_creator;
这样做的目的是为了在主程序中创建MyThread类对象时,可以传入一些有用的参数。
所以MyThread的构造函数写成这样
MyThread::MyThread(QtProgressThread* creator, QObject* parent) : QThread(parent)
{
m_thread_creator = creator;
}
增加一个信号,与以上槽函数updateProgress参数和返回值保持一致
signals:
void progress(int p, const char* msg);
重写run函数
void MyThread::run()
{
startWork();
}
这个startWork函数就是一个耗时任务了,如下。
void MyThread::startWork()
{
for (size_t i = 1; i <= 100; i++)
{
msleep(10);
emit progress(i, "Working...");
}
emit progress(100, "Done!");
}
这个函数用一段循环模拟耗时操作,同时,在执行的过程中,用Qt特有的emit语句发送progress信号。
接下来就是在主程序中利用MyThread类来创建新线程了。
在主程序类中声明成员变量 MyThread* my_thread,并在构造函数中new一个对象,并用connect语句连接信号和槽。
my_thread = new MyThread(this);
connect(my_thread, &MyThread::progress, this, &QtProgressTest::updateProgress);
为防止内存泄露,我在主程序类的析构函数中添加代码 delete my_thread;
最后一步,在doSomething槽函数中启动新线程
void QtProgressTest::doSomething()
{
my_thread->start();
}
编译并运行,效果如下。
以上就是QThread多线程进度条的简单实现。
在实际工程中,耗时任务并不是这么简单地可以在处理过程中利用emit语句发送信号,通常情况下耗时任务是底层的代码,并没有依赖Qt库,而是通过回调函数向外部抛出任务运行进度。
这种情况如何利用Qt实现多线程更新进度条呢?实际上可以新建一个QObject的派生类,在类函数中调用耗时任务的代码,然后再emit信号,相当于用Qt将底层代码包了一层。
创建QObject的派生类MyQtWorker,声明一个成员函数,作为回调函数,注意是静态的函数
static void progressCallback(int p, const char* msg);
声明一个静态成员变量,是类自身的指针
static MyQtWorker* this_worker;
在类的cpp文件中给这个变量赋值
MyQtWorker* MyQtWorker::this_worker = nullptr;
在类h文件中声明一个信号
signals:
void progress(int p, const char* msg);
回调函数的实现如下,在这个回调函数中emit信号
void MyQtWorker::progressCallback(int p, const char* msg)
{
if(this_worker)
emit this_worker->progress(p, msg);
}
还需要在类的构造函数中给刚才的静态变量赋值,就是把类自身的指针赋给她
MyQtWorker::MyQtWorker()
{
this_worker = this;
}
接下来模拟一个耗时任务的类TimeComsumingWork代码(不依赖Qt)
h文件
typedef void(* CallbackFun)(int, const char*);
class TimeComsumingWork
{
public:
void startWork(CallbackFun callback=nullptr);
};
cpp文件
#include "TimeComsumingWork.h"
#include "stdlib.h"
void TimeComsumingWork::startWork(CallbackFun callback)
{
for (size_t i = 0; i < 100; i++)
{
_sleep(10);
if (callback != nullptr)
callback(i, "Working...");
}
if (callback != nullptr)
callback(100, "Done!");
}
细心的你应该能发现,这个TimeComsumingWork的startWork函数其实就是把上面MyThread中run的内容抄了一遍。
接下来,就要让MyQtWork(依赖Qt)来调用TimeComsumingWork(不依赖Qt)的代码,并通过回调函数来发送信号。
给MyQtWork添加一个槽函数doMyJob,调用耗时任务代码并将MyQtWork的成员函数progressCallback作为回调传入startWork函数的参数中
void MyQtWorker::doMyJob()
{
TimeComsumingWork w;
w.startWork(progressCallback);
}
以上实现了MyQtWork对底层耗时任务代码的封装,可向外emit信号,接下来就应该回到MyThread中了,调用MyQtWork的doMyJob函数,并在主界面上订阅MyQtWork发送的进度信号。
在MyThread中增加一个成员函数,注意,最开始定义的m_thread_creator在这儿起到作用了。
void MyThread::startMyWork()
{
MyQtWorker w;
// 跨线程连接信号
connect(&w, &MyQtWorker::progress, m_thread_creator, &QtProgressTest::updateProgress);
w.doMyJob();
}
最后一步,在MyThread的run函数调用startMyWork即可。
void MyThread::run()
{
startMyWork();
}
运行效果图略。
基本思路是继承QObject类,将派生类的操作移动到新线程中。
前文中,已经实现了一个MyQtWork类,在这里正好可以利用起来。
对MyQtWork进行小小的改造,增加一个私有的成员变量QThread m_thread; 避免new delete的麻烦,这里我没有用指针。
给MyQtWork增加一个信号void done(); 在处理完成后emit这个信号。再给MyQtWork增加一个公有函数,用于启动线程。
void MyQtWorker::start()
{
m_thread.start();
}
将MyQtWorker的构造函数改造为:
MyQtWorker::MyQtWorker()
{
this_worker = this;
this->moveToThread(&m_thread);
connect(&m_thread, &QThread::started, this, &MyQtWorker::doMyJob);
connect(this, &MyQtWorker::done, &m_thread, &QThread::quit);
}
注意,这里先调用moveToThread,将MyQtWorker的代码移动到新线程中,并订阅两个信号,意思是当m_thread启动时,就执行耗时任务,当任务结束时,线程就退出。当外部调用MyQtWorker::start(),m_thread启动并触发doMyJob函数。
最后一步,在主程序中声明一个成员变量MyQtWorker* worker 并在构造函数中new
worker = new MyQtWorker;同时别忘了在析构中delete worker
同时connect一下MyQtWorker的信号
connect(worker, &MyQtWorker::progress, this, &QtProgressTest::updateProgress);
大功告成!
这样在MyQtWork中管理一个QThread对象,对外部调用者来说非常方便,不用再管理一个QThread指针的new和delete【这里参考了一篇文章的做法,见末尾】。很多文章在主程序中临时变量new一个QThread对象指针,并用到了QThread::deleteLater来自动释放new出来的QThread指针,那么在这种情况下就不要再手动delete了,程序会崩溃的。我一般的习惯是new和delete配对使用,所以就不采用这种方法了。
文中的代码已开源,下载地址:GitHub - CharlieV5/Qt: Qt example
参考:
Qt使用多线程的一些心得——2.继承QObject的多线程使用方法_尘中远的程序开发记录-CSDN博客_qobject线程
Qt新建线程的方法_hai200501019的专栏-CSDN博客