乐于分享
好东西不私藏

10分钟看懂Qt信号槽:从使用到源码级底层原理

10分钟看懂Qt信号槽:从使用到源码级底层原理

欢迎来到【Qt开发宝典】微信公众号【知识宝库】!

本期【C++高级工程师】动手实战系列,第153期。本期我们聚焦一个兼具实用性与学习深度的主题——👉10分钟看懂Qt信号槽:从使用到源码级底层原理本系列文章旨在为关注Qt跨平台开发、Linux C++后端及系统底层架构的开发者,提供一个系统的学习参考。内容将通过原理讲解与实战结合的方式,解析相关核心知识与设计思路,以期对您的技术探索与实践有所裨益。

👉欢迎关注【Qt开发宝典】微信公众号

🚀定期分享更多Linux C++/Qt等技术干货!!!

一、【工程项目运行】

【 运行界面】

【信号槽底层原理机制】

Qt 的信号与槽机制是 Qt 对象模型的核心能力,本质上是一套基于元对象系统(Meta-Object System)的类型安全回调机制。它既可以用于同一线程内对象之间的通信,也可以用于跨线程异步通信。

1. 元对象系统

Qt 的信号与槽依赖以下几个核心组件:

  • -Object:所有支持信号与槽的类通常都继承自 QObject

  • -Q_OBJECT:启用 Qt 元对象能力的宏。

  • -moc:Meta-Object Compiler,元对象编译器。

  • -QMetaObject:保存类名、信号、槽、属性等元信息的数据结构。

当一个类中使用了 Q_OBJECT 宏后,Qt 的 moc 工具会在编译前扫描头文件,并生成额外的 C++ 代码,例如:

- 类的元对象信息。

- 信号索引。

- 槽函数索引。

- 动态调用入口。

- 类型信息和参数描述。

因此,信号与槽并不是普通 C++ 语言原生提供的能力,而是 Qt 通过预编译工具和运行时元对象系统扩展出来的机制。

2. Q_OBJECT 的作用

Q_OBJECT 宏会为类注入 Qt 元对象系统所需的声明,使该类具备以下能力:

- 支持信号与槽。

- 支持运行时类型信息。

- 支持动态属性系统。

- 支持 qobject_cast

- 支持 QMetaObject::invokeMethod

- 支持对象间的自动连接和反射调用。

如果类中声明了 signals 或 slots,但没有写 Q_OBJECT,通常会导致链接错误或信号与槽无法正常工作。

3. moc 生成代码的基本思想

以本项目中的 Worker 为例:

classWorker : publicQObject {

    Q_OBJECT

public slots:

voiddoWork();

signals:

voidprogress(intvalue);

voidfinished();

};

moc 会为它生成类似如下功能的辅助代码:

voidWorker::progress(intvalue) {

void*args[] ={ nullptr,&value };

    QMetaObject::activate(this,&staticMetaObject, signal_index, args);

}

这段伪代码说明:

- 发射信号本质上会调用 QMetaObject::activate()

- Qt 根据当前对象、信号索引查找所有已连接的槽。

- 然后按照连接类型决定是立即调用槽函数,还是把调用封装为事件投递到目标线程。

4. connect 的底层含义

项目中有如下连接:

connect(this,&MainWindow::startTask, worker,&Worker::doWork, Qt::QueuedConnection);

connect(worker,&Worker::progress,this,&MainWindow::onTaskProgress, Qt::QueuedConnection);

connect(worker,&Worker::finished,this,&MainWindow::onTaskFinished, Qt::QueuedConnection);

connect 的作用是建立一条连接记录,大致包含:

- 发送者对象指针。

- 信号的元信息或函数指针。

- 接收者对象指针。

- 槽函数的元信息或函数指针。

- 连接类型。

这些连接关系会被 Qt 保存在发送者对象内部的连接列表中。当信号发射时,Qt 会遍历这些连接记录,并依次触发对应的槽函数。

5. 信号发射时发生了什么

当执行:

emitprogress(i);

需要注意:emit 不是 C++ 关键字,它在 Qt 中通常只是一个空宏,用于增强代码可读性。也就是说:

emitprogress(i);

从 C++ 编译角度看,近似等价于:

progress(i);

真正起作用的是 moc 为 progress(int) 生成的函数体。该函数内部会调用 Qt 元对象系统,找到所有连接到 progress(int) 的槽函数并触发它们。

6. 连接类型

Qt 常见连接类型包括:

Qt::AutoConnection默认类型若发送者和接收者在同一线程,则直接调用;否则使用队列调用。 

Qt::DirectConnection:立即在信号发射线程中调用槽函数。 

Qt::QueuedConnection将调用封装成事件,投递到接收者所属线程的事件队列中。 

Qt::BlockingQueuedConnection队列调用,但发送线程会阻塞等待槽函数执行完成。 

Qt::UniqueConnection避免重复建立相同连接。 

Qt::SingleShotConnection槽函数执行一次后自动断开连接。 

本项目中显式使用了 Qt::QueuedConnection,原因是主窗口对象和 Worker 对象位于不同线程。这样可以确保:

-Worker::doWork() 在工作线程中执行。

-MainWindow::onTaskProgress() 在主线程中执行。

- UI 控件始终由主线程更新。

7. 直接连接与队列连接的区别

 直接连接

如果使用 Qt::DirectConnection,槽函数会在信号发射的线程中立即执行。

示意:

线程 A 发射信号    ↓线程 A 立即执行槽函数

优点是调用开销较低,执行顺序直观。缺点是跨线程时容易误操作其他线程中的对象,尤其是 UI 控件。

队列连接

如果使用 Qt::QueuedConnection,Qt 不会立即调用槽函数,而是将调用信息包装成事件,投递到接收者对象所属线程的事件队列。

示意:

线程 A 发射信号    ↓Qt 创建调用事件    ↓投递到线程 B 的事件队列    ↓线程 B 的事件循环取出事件    ↓线程 B 执行槽函数

这种方式适合跨线程通信,也是 Qt 多线程 UI 程序中最常见、最安全的方式。

8. 跨线程通信为什么安全

Qt 中每个 QObject 都有线程归属,也就是 thread() 返回的线程对象。

当调用:

worker->moveToThread(workerThread);

worker 的线程归属变为 workerThread。之后,如果使用队列连接触发 Worker::doWork(),该槽函数会在 workerThread 的事件循环中执行。

对于本项目:

MainWindow 属于 GUI 主线程

Worker 属于后台工作线程

因此:

- 主线程发出 startTask()

- Qt 将 Worker::doWork() 调用投递到工作线程。

- 工作线程执行任务并发出 progress(int)

- Qt 将 MainWindow::onTaskProgress(int) 调用投递回主线程。

- 主线程更新进度条和状态文本。

这样既避免了界面阻塞,又避免了后台线程直接操作 UI。

9. 参数传递机制

信号与槽传参时,Qt 会根据连接类型采用不同方式:

- 直接连接:参数通常以普通函数调用方式传递。

- 队列连接:参数需要被复制或移动到事件对象中,随后在线程事件循环中再取出调用。

因此,跨线程队列连接传递自定义类型时,通常需要使用:

Q_DECLARE_METATYPE(MyType)

qRegisterMetaType<MyType>("MyType");

本项目中的信号参数是 int,属于 Qt 已知基础类型,所以不需要额外注册。

10. 自动断开连接

Qt 的信号与槽连接具有对象生命周期感知能力:

- 当发送者对象销毁时,相关连接会自动失效。

- 当接收者对象销毁时,相关连接也会自动断开。

这降低了悬空回调指针的风险,也是 Qt 信号与槽相比传统函数指针回调更安全的原因之一。

二、【工程项目简介】
1. 功能特性

- 使用 QMainWindow 构建标准桌面应用框架。

- 使用菜单栏和工具栏触发后台任务。

- 使用 QThread 承载后台任务对象。

- 使用信号与槽跨线程传递任务进度。

- 使用 QProgressBar 实时展示任务进度。

- 使用 QStatusBar 显示当前任务状态。

- 使用 Qt 样式表实现简洁现代的界面效果。

- 使用 Qt 资源文件加载窗口和菜单图标。

2. MainWindow

主窗口类,负责应用的整体组织:

- 创建中央控件。

- 创建菜单栏和工具栏。

- 设置窗口样式和状态栏。

- 初始化工作线程。

- 发送启动任务信号。

- 接收后台任务进度并更新界面。

关键通信流程:

用户点击“启动后台任务”        ↓MainWindow::onStartTask()        ↓emit startTask()        ↓Worker::doWork()        ↓emit progress(value)        ↓MainWindow::onTaskProgress(value)        ↓CentralWidget::updateProgress(value)

3. CentralWidget

中央内容区域,负责显示任务状态:

- 标题文本:后台任务执行进度。

- 进度条:显示 0 到 100 的任务进度。

- 状态标签:显示当前进度百分比。

- 提示文本:提示用户通过菜单或工具栏启动任务。

4. Worker

后台任务类,继承自 QObject,用于模拟耗时任务:

doWork() 在工作线程中执行。

- 每隔 50ms 发送一次进度更新信号。

- 进度从 0 增加到 100。

- 任务完成后发送 finished() 信号。

三、【工程项目】部分源码
1. main.cpp--程序主入口函数
// main.cpp - Qt应用程序入口文件// 包含主窗口类的头文件#include"mainwindow.h"// 包含Qt应用程序核心头文件#include<QApplication>// 程序主入口函数intmain(int argc, char *argv[]){    // 创建Qt应用程序对象,管理整个应用程序的生命周期    // argc 和 argv 是命令行参数,Qt可能使用它们初始化某些设置    QApplication a(argc, argv);    // 设置全局应用程序样式为Fusion风格(可选)    // Fusion是Qt提供的一个现代跨平台样式,比原生样式更统一美观    QApplication::setStyle("Fusion");    // 创建主窗口对象    MainWindow w;    // 设置应用程序窗口图标    // 注意:这里使用了Qt资源系统路径语法(:/开头)    // 需要确保项目资源文件中已正确添加logo2.ico文件    // 格式:":/[资源前缀]/[目录结构]/文件名.扩展名"    w.setWindowIcon(QIcon(":/new/prefix1/images/logo2.ico"));    // 显示主窗口    w.show();    // 进入Qt主事件循环,等待用户交互    // exec()会阻塞直到应用程序退出(窗口关闭)    return a.exec();}
2. mainwindow.h
#ifndef MAINWINDOW_H#define MAINWINDOW_H#include<QMainWindow>#include<QAction>#include<QStatusBar>#include<QThread>#include"./CentralWidget/CentralWidget.h"#include"./Worker/Worker.h"class MainWindow : public QMainWindow {    Q_OBJECTpublic:    explicitMainWindow(QWidget *parent = nullptr);    ~MainWindow();signals:  // 添加信号声明    voidstartTask();private slots:    voidonStartTask();        // 启动任务    voidonTaskProgress(int);  // 更新进度    voidonTaskFinished();     // 任务完成private:    voidcreateMenu();         // 创建菜单    voidcreateToolBar();      // 创建工具栏    voidapplyStyle();         // 应用整体样式    voidinitWorkerThread();   // 初始化工作线程    // 成员变量    CentralWidget *centralWidget;    QAction *startTaskAction;    QThread *workerThread;    Worker *worker;};#endif// MAINWINDOW_H
3. mainwindow.cpp
#include"MainWindow.h"#include<QMenuBar>#include<QToolBar>#include<QMessageBox>#include<QStatusBar>#include<QFont>MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {    setWindowTitle("Qt6——工业级信号与槽架构工程项目");    resize(900560);    // 创建中央控件    centralWidget = new CentralWidget(this);    setCentralWidget(centralWidget);    // 初始化界面    createMenu();    createToolBar();    applyStyle();    statusBar()->showMessage("准备就绪");    // 初始化工作线程    initWorkerThread();}MainWindow::~MainWindow() {    workerThread->quit();    workerThread->wait();    delete worker;}voidMainWindow::createMenu(){    QMenuBar *bar = menuBar();    QMenu *taskMenu = bar->addMenu("任务(&T)");    startTaskAction = new QAction(QIcon(":/new/prefix1/images/logo.ico"), "启动后台任务"this);    startTaskAction->setShortcut(QKeySequence("Ctrl+R"));    startTaskAction->setStatusTip("启动一个模拟的耗时后台任务,并在界面中实时显示进度");    taskMenu->addAction(startTaskAction);    connect(startTaskAction, &QAction::triggered, this, &MainWindow::onStartTask);}voidMainWindow::createToolBar(){    QToolBar *toolBar = addToolBar("操作");    toolBar->setMovable(false);    toolBar->setIconSize(QSize(2424));    toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);    toolBar->addAction(startTaskAction);}voidMainWindow::applyStyle(){    // 主窗口整体配色与控件样式    setStyleSheet(        "QMainWindow {"        "   background: transparent;"        "}"        "QMenuBar {"        "   background: #ffffff;"        "   border-bottom: 1px solid #dfe4ec;"        "   padding: 4px 8px;"        "   font: 12px 'Microsoft YaHei';"        "}"        "QMenuBar::item {"        "   padding: 4px 10px;"        "   margin: 0 2px;"        "   border-radius: 4px;"        "}"        "QMenuBar::item:selected {"        "   background: #e6f0ff;"        "}"        "QMenu {"        "   background: #ffffff;"        "   border: 1px solid #d0d7e2;"        "   padding: 6px 0;"        "   font: 12px 'Microsoft YaHei';"        "}"        "QMenu::item {"        "   padding: 6px 24px;"        "}"        "QMenu::item:selected {"        "   background: #e6f0ff;"        "}"        "QToolBar {"        "   background: #f7f9fc;"        "   border-bottom: 1px solid #dfe4ec;"        "   padding: 4px 8px;"        "}"        "QToolButton {"        "   padding: 6px 10px;"        "   border-radius: 6px;"        "   color: #2c3e50;"        "   font: 12px 'Microsoft YaHei';"        "}"        "QToolButton:hover {"        "   background: #e6f0ff;"        "}"        "QToolButton:pressed {"        "   background: #d0e0ff;"        "}"        "QStatusBar {"        "   background: #ffffff;"        "   border-top: 1px solid #dfe4ec;"        "   font: 11px 'Microsoft YaHei';"        "   color: #7f8c8d;"        "}"    );    // 状态栏附加一段品牌/提示文本    QLabel *brand = new QLabel("Qt C++ 架构实践 · Signals/Slots Demo"this);    QFont brandFont("Microsoft YaHei"9);    brand->setFont(brandFont);    brand->setStyleSheet("color: #95a5a6;");    statusBar()->addPermanentWidget(brand);}voidMainWindow::initWorkerThread(){    workerThread = new QThread(this);    worker = new Worker();    worker->moveToThread(workerThread);    // 连接信号与槽(跨线程必须使用 QueuedConnection)    connect(this, &MainWindow::startTask, worker, &Worker::doWork, Qt::QueuedConnection);    connect(worker, &Worker::progress, this, &MainWindow::onTaskProgress, Qt::QueuedConnection);    connect(worker, &Worker::finished, this, &MainWindow::onTaskFinished, Qt::QueuedConnection);    workerThread->start();}voidMainWindow::onStartTask(){    statusBar()->showMessage("任务运行中...");    emit startTask();  // 触发工作线程}voidMainWindow::onTaskProgress(int value){    centralWidget->updateProgress(value);}voidMainWindow::onTaskFinished(){    statusBar()->showMessage("任务完成");    QMessageBox::information(this"提示""耗时任务已完成!");}
希望这篇文章能为您的Linux C/C++、Qt开发等学习之旅提供有价值的参考!感谢各位关注👉【Qt开发宝典】微信公众号,获取更多技术干货。