Qt处理耗时任务,防止界面假死的两个简单写法

借助QtConcurrentQApplication::processEvents()简化处理耗时任务以防界面假死。

问题

有如下需求

点击开始按钮后,执行耗时操作,该按钮进行中后的.的数量随时间变化

start

doing

代码如下

mainwindow.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QCloseEvent>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();

private:
void doSomeThing0();
void toDoOneThing(int _index);

private:
Ui::MainWindow *ui;

const std::vector<QString> DOT_VECTOR =
{
".",
"..",
"...",
"....",
".....",
};

const int SLEEP_TIME = 20;
};

#endif // MAINWINDOW_H

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QtConcurrent>
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton, &QPushButton::clicked, [=]() {
ui->pushButton->setEnabled(false);

doSomeThing0();

ui->pushButton->setText(u8"开始");
ui->pushButton->setEnabled(true);
});
}

MainWindow::~MainWindow()
{
delete ui;
}

void MainWindow::doSomeThing0()
{
for (size_t i = 0; i < 10000; i++)
{
toDoOneThing(i);
}
}

void MainWindow::toDoOneThing(int _index)
{
//变化按钮文字后面"."的个数
QString buttonText = u8"进行中";
buttonText += DOT_VECTOR[_index / 20 % DOT_VECTOR.size()];
ui->pushButton->setText(buttonText);

//模拟耗时操作
QThread::msleep(SLEEP_TIME);
}

以上代码运行后点击开始,界面假死。

很多情况下,我们要在界面种处理某些耗时的操作,如果直接调用的话,主界面就会卡死,一般来说通过多线程可以解决该问题,包括最传统的run()和新式的QObject::moveToThread()具体可参考该文章Qt 多线程编程之敲开 QThread 类的大门 - 知乎 (zhihu.com)

有时候耗时操作逻辑很简单,代码也没有几行,单独封装一个类有点麻烦,有没有更省事的方法呢?

单线程方式

可以单线程借助QApplication::processEvents()实现以上需求:

mainwindow.h

1
2
3
4
5
6

private:
void doSomeThing0();
void doSomeThing1(); //新增
void toDoOneThing(int _index);

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QtConcurrent>
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton, &QPushButton::clicked, [=]() {
ui->pushButton->setEnabled(false);

doSomeThing1(); //调用doSomeThing1()

ui->pushButton->setText(u8"开始");
ui->pushButton->setEnabled(true);
});
}

void MainWindow::doSomeThing0()
{
for (size_t i = 0; i < 10000; i++)
{
toDoOneThing(i);
}
}

void MainWindow::doSomeThing1()
{
for (size_t i = 0; i < 10000; i++)
{
toDoOneThing(i);
QApplication::processEvents(); //新增
}
}

可以看出doSomeThing1()doSomeThing0()只多一行代码。

多线程方式

这里使用QtConcurrent::run()函数,实现起来相当简洁:

.pro

1
QT += concurrent

mainwindow.h

1
2
3
4
5
private:
void doSomeThing0();
void doSomeThing2();//新增
void toDoOneThing(int _index);

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QtConcurrent>
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton, &QPushButton::clicked, [=]() {
ui->pushButton->setEnabled(false);

doSomeThing2(); //调用doSomeThing2()

ui->pushButton->setText(u8"开始");
ui->pushButton->setEnabled(true);
});
}

void MainWindow::doSomeThing0()
{
for (size_t i = 0; i < 10000; i++)
{
toDoOneThing(i);
}
}

void MainWindow::doSomeThing2()
{
auto future = QtConcurrent::run(this, &MainWindow::doSomeThing0);

while (future.isFinished() == false)
{
QApplication::processEvents();
QThread::msleep(20);
}
}

Concurrent是并发的意思,QtConcurrent是一个命名空间,提供了一些高级的 API,使得在编写多线程的时候,无需使用低级线程原语,如读写锁,等待条件或信号。使用QtConcurrent编写的程序会根据可用的处理器内核数自动调整使用的线程数。Qt QtConcurrent之 Run 函数用法_luoyayun361的专栏-CSDN博客_qtconcurrent

QtConcurrent::run()从第三个参数开始为所调用函数的参数。

小问题

以上代码执行时会出现点击关闭按钮无法退出的问题,解决方法如下:

mainwindow.h

1
2
3
4
5
6
protected:
void closeEvent(QCloseEvent *event);

private:
bool isFinished_ = false;

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void MainWindow::closeEvent(QCloseEvent * event)
{
isFinished_ = true;
return QMainWindow::closeEvent(event);
}

void MainWindow::doSomeThing0()
{
for (size_t i = 0; i < 10000; i++)
{
if (isFinished_ == true)
break;
toDoOneThing(i);
}
}

void MainWindow::doSomeThing1()
{
for (size_t i = 0; i < 10000; i++)
{
if (isFinished_ == true)
break;
toDoOneThing(i);

QApplication::processEvents();
}
}

示例代码路径

Backgrounder · yuque/demo - 码云 - 开源中国 (gitee.com)