1.前言
在 Qt 开发中,GUI 程序有一个基本准则:所有耗时的操作都不应放在主线程(GUI 线程)中执行。 主线程负责维护事件循环和处理界面更新,一旦被耗时任务阻塞,界面就会卡死导致无法拖动、按钮无响应、进度条不刷新等问题。
2.原因
Qt 程序的 main() 函数末尾通常会调用 QApplication::exec()启动一个事件循环(event loop)。事件循环不断从队列中取出事件(鼠标点击、重绘、定时器、信号槽调用等)并处理。如果我们在主线程的某个槽函数里写了一个耗时的任务,那么在这个循环结束之前,事件循环无法处理任何新事件,导致界面无法重绘,呈现出卡死状态。
3.解决方法
QThread+moveToThread
4.代码示例
MainWindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
class Worker;
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void StartButtonClicked();// 点击开始计算按钮
void updateProgress(int percent);// 进度条更新百分比
void showResult(long long result);// 展示累加计算结果
void onWorkerFinished();// 任务完成,按钮恢复
private:
Ui::MainWindow *ui;
QThread *worker_thread_;// 工作线程
Worker *worker_;// 工作对象
};
#endif
MainWindow.cpp
cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "Worker.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 点击开始计算按钮
connect(ui->startButton,&QPushButton::clicked,this,&MainWindow::StartButtonClicked);
// 初始化线程和工作者对象
worker_thread_ = new QThread(this);// 方便delete ui;时自动删除 worker_thread_
worker_ = new Worker();// 不能有父对象,否则不能 moveToThread
// 将工作对象移动到工作线程
if(worker_) worker_->moveToThread(worker_thread_);
// 当工作线程结束时,自动清理掉工作对象,防止内存泄漏
connect(worker_thread_, &QThread::finished, worker_, &QObject::deleteLater);
// 进度条更新百分比
connect(worker_, &Worker::progressUpdated, this, &MainWindow::updateProgress);
// 发送累加计算结果
connect(worker_, &Worker::resultReady, this, &MainWindow::showResult);
// 任务完成,按钮恢复
connect(worker_, &Worker::finished, this, &MainWindow::onWorkerFinished);
}
MainWindow::~MainWindow()
{
// 结束线程并安全销毁
if (worker_thread_ && worker_thread_->isRunning())
{
worker_thread_->quit();// 自动调用worker_thread_的finished信号清理工作对象,无需再手动delete worker_
worker_thread_->wait();// 阻塞等待,直到线程完全退出
}
delete ui;// 对象树删除 worker_thread_
}
// 点击开始计算按钮
void MainWindow::StartButtonClicked()
{
// 禁用开始按钮,防止重复启动
ui->startButton->setEnabled(false);
// 清空计算结果
ui->statusLabel->clear();
// 启动线程
if(worker_thread_ && !worker_thread_->isRunning()) worker_thread_->start();
// 通过信号槽调用 Worker::doHeavyLoop,这个槽会在工作线程中执行
// 参数 200000000 表示 2 亿次循环,模拟耗时任务
QMetaObject::invokeMethod(worker_, "doHeavyLoop",
Qt::QueuedConnection,
Q_ARG(long, 200000000));
}
// 进度条更新百分比
void MainWindow::updateProgress(int percent)
{
ui->progressBar->setValue(percent);
}
// 展示累加计算结果
void MainWindow::showResult(long long result)
{
ui->statusLabel->setText(QString("计算结果: %1").arg(result));
}
// 任务完成,按钮恢复
void MainWindow::onWorkerFinished()
{
ui->startButton->setEnabled(true);
}
Worker.h
cpp
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
public slots:
// 该槽函数会在工作线程中执行
void doHeavyLoop(long loopCount);
signals:
// 进度条更新百分比
void progressUpdated(int percent);
// 发送累加计算结果
void resultReady(long long result);
// 任务完成信号
void finished();
};
#endif
Worker.cpp
cpp
#include "Worker.h"
#include <QThread>
Worker::Worker(QObject *parent) : QObject(parent)
{
}
void Worker::doHeavyLoop(long loopCount)
{
// 模拟一个耗时的累加循环
long long sum = 0;
for (long long i = 1; i <= loopCount; ++i)
{
sum += i;
// 每迭代 1% 发送一次进度,避免信号过密
if (i % (loopCount / 100) == 0)
{
int percent = (i * 100) / loopCount;
emit progressUpdated(percent);
// 额外加一个微小延时,让信号有机会得到处理(非必须,但便于观察)
// QThread::msleep(1000);// 1秒
}
}
// 发送累加计算结果
emit resultReady(sum);
// 任务完成,停止线程
emit finished();
}
5.运行结果
