C++多线程(1)——Qt利用多线程更新进度条

在编写界面程序时,通常会运行一些代码执行比较耗时的任务。如果想在执行任务的过程中向用户提示当前处理进度,通常会在界面上增加一个进度条。如果这个任务运行在主程序的线程中,就会造成主界面的卡死,进度条根本无法实时更新,直到任务执行完界面才能恢复。这个时候就应该利用多线程技术,将耗时任务的代码放到一个新的线程中运行,同时向主程序线程更新进度。

接下来我用Qt中QThread来实现这个目的。

我用的编程环境是Visual Studio 2019 Community和Qt 5.12。

主界面

在这里我设计一个按钮来启动一个耗时任务,并在主界面上实时刷新进度条以显示任务运行的进度。

首先,创建一个带界面的Qt桌面程序工程,选【Qt Widgets Application】。我将工程重新命名为QtProgressTest。用Qt Designer打开.ui文件,在主界面上增加一个Push Button和Progress Bar控件,另外增加一个Label控件以显示一些提示文字。如下图。

C++多线程(1)——Qt利用多线程更新进度条_第1张图片

 接下来添加这个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

基本思路是继承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();
}

编译并运行,效果如下。

C++多线程(1)——Qt利用多线程更新进度条_第2张图片

以上就是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();
}

运行效果图略。

第二种方法 用moveToThread

基本思路是继承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博客

你可能感兴趣的:(C++,qt,c++,多线程)