编写QT软件的时候,经常会遇到点击某个按钮,进行一个比较耗时的计算。为了在计算过程中,软件界面就继续响应用户的点击,不会有卡死的感觉,一般会将这个耗时的计算放在另外一个线程里面,同时在界面上布置一个进度条(QProgressBar),显示当前的计算进度,提高软件的界面以及响应性。
下面就这一看似简单实则暗藏玄机的编程过程进行抽丝剥茧的解释。
使用QT时,在线程里面执行计算的时候,修改界面元素,有多种方法,最常见的就是两种:1)继承QThread
,然后重写run
,把耗时的计算代码放在run
函数里面,同时定义进度更新的信号;2)定义一个继承自QObject
的Worker类,里面定义进度更新信号,然后使用moveToThread
的方法,将该类绑定到另外一个线程,在主线程里面,将按钮的信号和Worker的槽函数连接后,按钮信号的触发,就会触发Worker的槽函数在其绑定的线程里面执行。
下面是使用moveToThread的方法来定义一个Worker类:
class Worker : public QObject
{
Q_OBJECT
public:
~Worker() { std::cout << "worker is destructed!\n"; }
public slots:
void stop() { m_stopped = true; }
int long_time_compution()
{
m_stopped = false;
std::cout << "start long time compution ...\n";
int i = 0;
while (!m_stopped && i++ < 100)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
emit progressValueUpdated(i);
std::cout << std::format("i: {}\n", i);
}
std::cout << std::format("finish long time compution, result: {}\n", i);
emit resultReady(QString("Result: %1").arg(i));
return i;
}
signals:
void progressValueUpdated(int);
void resultReady(QString);
private:
std::atomic_bool m_stopped{false};
};
我们定义了两个槽函数start和stop,以及两个信号progressValueUpdated和resultReady。
再看一下界面窗口的定义以及如何连接信号和槽函数:
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QMainWindow win;
auto widget = new QWidget;
auto layout = new QVBoxLayout;
widget->setLayout(layout);
win.setCentralWidget(widget);
QProgressBar *bar = new QProgressBar;
layout->addWidget(bar);
auto btn_start = new QPushButton("Start");
layout->addWidget(btn_start);
auto btn_stop = new QPushButton("Stop");
layout->addWidget(btn_stop);
auto result_label = new QLabel("Result:");
layout->addWidget(result_label);
// 下面的worker要绑定的线程
QThread worker_thread;
Worker worker;
// 将worker绑定到单独的线程
worker.moveToThread(&worker_thread);
// 开始按钮的点击信号和worker的start槽函数连接起来
QObject::connect(btn_start, &QPushButton::clicked, &worker, &Worker::long_time_compution);
// 停止按钮的点击信号和worker的stop槽函数连接起来
QObject::connect(btn_stop, &QPushButton::clicked, &worker, &Worker::stop, Qt::DirectConnection);
// worker的进度更新信号和界面上的进度条连接起来
QObject::connect(&worker, &Worker::progressValueUpdated, bar, &QProgressBar::setValue);
QObject::connect(&worker, &Worker::resultReady, result_label, &QLabel::setText);
// 开启工作线程的事件循环
worker_thread.start();
win.show();
app.exec();
// 退出工作线程的事件循环
worker_thread.quit();
// 等待线程当前执行的任务完成并退出
worker_thread.wait();
}
上面的代码第一部分是界面设置,第二部分的连接界面和Worker的信号与槽,需要注意的是QObject::connect(btn_stop, &QPushButton::clicked, &worker, &Worker::stop, Qt::DirectConnection)
这里最后一个参数是Qt::DirectConnection
——直接连接,意思是在信号发送者的线程中调用接收者的槽函数。默认的参数是Qt::AutoConnection
,如果发送者和接收者不在同一个线程中,会使用Qt::QueuedDirection
,槽函数的调用会在接收者所在的线程中执行;如果两个在同一个线程中,那就是Qt::DirectConnection
,槽函数是立即在发送者的线程中调用的。这里指定为Qt::DirectConnection
是必须的,首先因为调用的函数是非耗时的,且线程安全(修改的变量是atomic_bool),所以不会出现多线程读写同步的问题;另外,如果不使用Qt::DirectConnect
,那么默认会使用Qt::QueuedConnection
,停止按钮点击时,触发一个调用Worker::stop的事件,该事件需要work_thread进行处理,可work_thread此时正在进行耗时计算,只有当计算完毕之后,才会处理事件,调用stop,显然已经没有意义了,stop的目的就是在计算过程当中来随时终止计算。