2. 线程中的信号槽连接

任务目标

创建一个Qt控制台或Widgets程序,在子线程中通过信号发射数据,主线程接收该信号并更新UI(此处以QLabel文本更新为例)。核心是理解:当信号与槽位于不同线程时,Qt默认使用队列连接(QueuedConnection),信号发射后不会立即执行槽函数,而是投递到接收者所在线程的事件循环中,由事件循环调度执行。

环境与依赖

  • Qt 5.12+(Widgets模块)
  • 编译器:MSVC / MinGW / GCC 均可
  • 项目文件(.pro)需包含 QT += widgets

关键完整代码

// worker.h
#ifndef WORKER_H
#define WORKER_H

#include <QObject>
#include <QThread>

class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr);

public slots:
    void doWork();          // 在子线程中执行的入口

signals:
    void progressUpdated(int value);   // 跨线程信号
    void finished();
};

#endif // WORKER_H

// worker.cpp
#include "worker.h"
#include <QThread>
#include <QDebug>

Worker::Worker(QObject *parent) : QObject(parent) {}

void Worker::doWork()
{
    // 模拟耗时计算,每200ms发射一次信号
    for (int i = 0; i <= 100; i += 20) {
        qDebug() << "子线程发射信号,值:" << i
                 << ",线程ID:" << QThread::currentThreadId();
        emit progressUpdated(i);
        QThread::msleep(200);
    }
    emit finished();
}

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QLabel>
#include <QPushButton>
#include <QThread>
#include "worker.h"

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void startWork();
    void updateLabel(int value);
    void onWorkerFinished();

private:
    QLabel *m_label;
    QPushButton *m_startBtn;
    QThread m_workerThread;
    Worker *m_worker;
};

#endif // MAINWINDOW_H

// mainwindow.cpp
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , m_label(new QLabel("等待开始...", this))
    , m_startBtn(new QPushButton("开始工作", this))
    , m_worker(nullptr)
{
    // 界面布局
    auto *central = new QWidget(this);
    auto *layout = new QVBoxLayout(central);
    layout->addWidget(m_label);
    layout->addWidget(m_startBtn);
    setCentralWidget(central);

    // 创建Worker并移动到子线程
    m_worker = new Worker;
    m_worker->moveToThread(&m_workerThread);

    // 连接信号槽(自动使用QueuedConnection)
    connect(m_startBtn, &QPushButton::clicked,
            this, &MainWindow::startWork);
    connect(this, &MainWindow::startWork,
            m_worker, &Worker::doWork);
    connect(m_worker, &Worker::progressUpdated,
            this, &MainWindow::updateLabel);
    connect(m_worker, &Worker::finished,
            this, &MainWindow::onWorkerFinished);

    // 线程结束时清理
    connect(&m_workerThread, &QThread::finished,
            m_worker, &QObject::deleteLater);
}

MainWindow::~MainWindow()
{
    m_workerThread.quit();
    m_workerThread.wait();
}

void MainWindow::startWork()
{
    m_startBtn->setEnabled(false);
    m_workerThread.start();
}

void MainWindow::updateLabel(int value)
{
    qDebug() << "主线程接收信号,值:" << value
             << ",线程ID:" << QThread::currentThreadId();
    m_label->setText(QString("当前进度: %1%").arg(value));
}

void MainWindow::onWorkerFinished()
{
    m_workerThread.quit();
    m_workerThread.wait();
    m_startBtn->setEnabled(true);
    m_label->setText("工作完成");
}

// main.cpp
#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

运行与验证

  1. 创建Qt Widgets项目,将上述5个文件(worker.h/cpp、mainwindow.h/cpp、main.cpp)加入项目,并确保.pro文件包含 QT += widgets。
  2. 编译并运行程序,点击“开始工作”按钮。
  3. 观察qDebug输出:子线程发射信号时打印的线程ID与主线程接收信号时打印的线程ID不同,证明信号确实跨线程传递。
  4. 观察UI:QLabel文本依次更新为20%、40%、60%、80%、100%,最后变为“工作完成”。若UI能正常刷新且无崩溃,说明队列连接机制工作正常。

通过判定标准:程序运行无崩溃,UI按预期更新,且控制台输出中发射与接收的线程ID不同。

关键机制说明

当connect时,Qt根据发射者与接收者是否在同一线程自动选择连接方式。本例中m_worker已moveToThread到子线程,而MainWindow在主线程,因此progressUpdated信号与updateLabel槽之间自动使用QueuedConnection——信号发射后立即返回,参数被封装为事件投递到主线程事件队列,由主线程事件循环取出并执行槽函数。这正是跨线程UI更新的安全方式。