Qt 信号与槽机制详解教程

Qt 的信号与槽(Signals and Slots)机制是 Qt 框架的核心特性之一,它提供了一种高效的对象间通信方式,使得组件之间能够以松散耦合的形式交互。这种机制受到了 Observer 设计模式的影响,但 Qt 通过元对象系统(Meta-Object System,简称 MOC)使其更易用和类型安全。以下是基于提供的原始内容的扩展版本,每个章节都添加了更深入的解释、背景知识、潜在问题讨论,以及更丰富的实例代码和测试代码。所有代码示例均为完整的、可编译的 Qt 程序(假设使用 Qt 6.x),并包括预期输出和测试场景。代码使用 Qt Widgets 模块,编译时需链接 QtWidgets 库(例如:qmakecmake 配置)。

一、概念

Qt 的信号与槽(Signals and Slots)机制是一个用于对象间通信的核心特性。这个机制使得对象能以松散耦合的方式进行通信,从而提升了代码的模块化和可维护性。信号与槽的概念源于事件驱动编程,在 Qt 中,它允许一个对象在状态变化时“通知”其他对象,而无需知道这些对象的具体实现细节。这使得代码更易扩展,例如添加新组件时无需修改现有代码。

  • 信号(Signal):对象状态的变化或事件发生时发出的通知。信号本质上是一个函数声明,但不实现具体逻辑。它可以携带参数,用于传递数据。
  • 槽(Slot):信号发出时调用的函数,用于处理信号。槽是一个普通的成员函数,可以是 public、protected 或 private。
  • 连接(Connect):将信号和槽关联起来,使得信号发出时自动调用对应的槽。连接可以是静态的(编译期)或动态的(运行期),支持一对多、多对一等模式。

扩展解释

  • 信号与槽的松耦合性意味着发送者和接收者无需相互引用头文件,这在大型项目中减少了编译依赖。
  • 与回调函数不同,信号与槽是类型安全的:编译时检查参数匹配,运行时自动处理线程安全(通过 Qt::QueuedConnection 等)。
  • 潜在问题:如果信号参数过多,可能导致性能开销;建议保持信号简单。

二、原理机制

信号与槽机制依赖于 Qt 的元对象系统(Meta-Object System),该系统通过 Q_OBJECT 宏和 MOC(Meta-Object Compiler)生成额外的代码来支持信号与槽功能。MOC 是一个预处理器,它扫描包含 Q_OBJECT 的类,生成元对象代码(如 QMetaObject),用于运行时查询信号、槽和属性。

扩展解释

  • Q_OBJECT 宏:必须在类声明中添加,它展开为 QMetaObject 的定义和一些辅助函数。如果遗漏,信号与槽将无法工作。
  • MOC 的作用:生成 moc_xxx.cpp 文件,包含信号的发射逻辑和元信息。编译时,这些文件被链接到项目中。
  • 线程安全:Qt 支持跨线程信号与槽(Qt::QueuedConnection),内部使用事件队列,避免直接调用。
  • 性能考虑:信号发射有轻微开销(元对象查找),但在 GUI 应用中通常可忽略。避免在高频循环中使用。
  • 潜在问题:如果类未继承 QObject,Q_OBJECT 无效;MOC 不支持模板类(需手动处理)。

三、工作流程

一个典型的信号与槽工作流程包括四个步骤:

  1. 信号声明:在类中使用 signals 关键字声明信号。信号是纯虚函数,不需要实现。
  2. 槽声明和定义:在类中用 public slots、private slots 或 protected slots 声明槽,并实现槽函数。槽可以是虚函数,支持继承。
  3. 连接信号和槽:使用 QObject::connect() 函数将信号和槽连接起来。connect() 返回 bool,表示连接是否成功。
  4. 发射信号:在适当时机调用 emit 信号,触发相应的槽函数。emit 是宏,展开为 MOC 生成的发射代码。

扩展解释

  • 连接模式:Qt::DirectConnection(同步)、Qt::QueuedConnection(异步,跨线程安全)、Qt::UniqueConnection(避免重复连接)。
  • 断开连接:使用 disconnect(),支持动态管理。
  • 信号阻塞:QObject::blockSignals(true) 可临时禁用信号发射。
  • 潜在问题:循环连接(信号 A 触发 B,B 触发 A)可能导致栈溢出;使用 Qt::QueuedConnection 避免。

四、实际项目示例

我们将通过一个简单的 Qt 应用程序来演示信号与槽机制。这个程序包含一个按钮和一个标签,当点击按钮时,标签显示的文字会改变。扩展后,我们添加了多个按钮和标签,演示一对多连接。

步骤 1:创建 Qt 项目
打开 Qt Creator,选择 “File” -> “New File or Project”。
选择 “Application” -> “Qt Widgets Application”。
输入项目名称(例如 “SignalSlotExample”)。
选择项目路径并点击 “Next”。
选择适合的平台和 Qt 版本,点击 “Next”。
点击 “Finish” 创建项目。

步骤 2:设计用户界面
在项目树中,打开 mainwindow.ui 文件。
拖动一个 QPushButton 和一个 QLabel 到窗口中。
设置按钮和标签的文本为 “Click Me!” 和 “Hello, Qt!”。
扩展:添加第二个 QPushButton(文本 “Reset”)和第二个 QLabel(文本 “Status: Idle”)。

步骤 3:添加按钮功能

1. 使用“转到槽”功能

“转到槽”功能允许你快速为UI控件添加槽函数,并自动生成必要的代码。

步骤
打开 UI 设计器:在项目树中,双击打开 mainwindow.ui 文件。
选择控件:在 UI 设计器中,选择想要添加槽函数的控件,比如 QPushButton。
右键菜单:右键点击选择的控件,并在弹出的右键菜单中选择“转到槽…”(Go to Slot…)。
选择信号:在弹出的对话框中,选择你希望处理的信号,例如 clicked(),然后点击“确定”或“添加”(Add)。
Qt Creator 将自动生成槽函数的声明和实现代码,并将你带到实现代码的位置。

执行上述步骤后,mainwindow.h 文件将自动添加新槽函数的声明,如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

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 on_pushButton_clicked();
    void on_resetButton_clicked();  // 扩展:第二个按钮的槽

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

对应的槽函数实现也会自动添加到 mainwindow.cpp 文件中,修改 mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

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

void MainWindow::on_pushButton_clicked()
{
    // 修改标签的文本
    ui->label->setText("Button Clicked!");
    ui->statusLabel->setText("Status: Clicked");  // 扩展:更新第二个标签
}

void MainWindow::on_resetButton_clicked()
{
    // 重置标签
    ui->label->setText("Hello, Qt!");
    ui->statusLabel->setText("Status: Idle");
}

完整 main.cpp(不变):

#include "mainwindow.h"
#include <QApplication>

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

测试代码与输出
运行程序,点击 “Click Me!” 按钮,标签变为 “Button Clicked!”,状态标签变为 “Status: Clicked”。点击 “Reset”,恢复原状。
测试场景:快速点击按钮,观察响应;关闭窗口,检查内存泄漏(使用 Valgrind 或 Qt Debug)。

2. 自定义“添加信号”到槽功能

虽然“自定义添加信号”功能没有像“转到槽”那样的快捷选项,但你可以手动添加信号声明,并在代码中进行连接。

手动添加信号

修改 mainwindow.h 文件:在类中添加新的信号声明。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

signals:
    void customSignal(); // 新的信号声明

private slots:
    void on_pushButton_clicked();
    void on_resetButton_clicked();
    void handleCustomSignal();  // 扩展:自定义槽

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

发射信号:在 mainwindow.cpp 文件中,发射自定义信号。我们可以在按钮点击槽函数中发射这个信号:

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 连接自定义信号到某个槽
    connect(this, &MainWindow::customSignal, this, &MainWindow::handleCustomSignal);
    connect(ui->pushButton, &QPushButton::clicked, this, &MainWindow::handleCustomSignal);  // 扩展:直接连接内置信号
}

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

void MainWindow::on_pushButton_clicked()
{
    // 修改标签的文本
    ui->label->setText("Button Clicked!");
    // 发射自定义信号
    emit customSignal();
}

void MainWindow::on_resetButton_clicked()
{
    // 重置
    ui->label->setText("Hello, Qt!");
}

void MainWindow::handleCustomSignal()
{
    // 处理自定义信号
    ui->statusLabel->setText("Custom Signal Emitted!");
}

自定义添加信号到槽关键步骤

  1. 声明信号
    在 mainwindow.h 中声明新的信号
signals:
    void customSignal(); // 新的信号声明
  1. 声明槽
    在 mainwindow.h 中声明新的槽
private slots:
    void on_pushButton_clicked();
    void handleCustomSignal(); // 新的槽声明
  1. 处理自定义信号(槽的实现)
void MainWindow::handleCustomSignal()
{
    // 处理自定义信号
    ui->statusLabel->setText("Custom Signal Emitted!");
}
  1. 连接自定义信号到指定槽
// 连接自定义信号到指定槽
connect(this, &MainWindow::customSignal, this, &MainWindow::handleCustomSignal);
  1. 发射信号
// 发射自定义信号
emit customSignal();

测试代码与输出
运行程序,点击按钮,发射 customSignal,更新状态标签。测试:断开连接(disconnect(this, &MainWindow::customSignal, this, &MainWindow::handleCustomSignal);),观察信号不触发。

步骤 4:运行程序
点击 Qt Creator 工具栏上的 “Run” 按钮。
在弹出的窗口中,点击按钮,观察标签的文本变化。

详细解释
信号声明:在 Qt 中,信号是通过在类中使用 signals 关键字声明的。在这个例子中,QPushButton 已经有了一个名为 clicked() 的信号。
槽声明和定义:槽是一个普通的成员函数,可以在类中用 public slots、private slots 声明。在这个例子中,我们在 MainWindow 类中声明并定义了一个槽 handleButtonClick()。
连接信号和槽:使用 QObject::connect() 函数将信号和槽连接起来。参数包括发出信号的对象、信号名称、接收信号的对象和槽名称。
发射信号:当按钮被点击时,QPushButton 内部自动发射 clicked() 信号,触发 handleButtonClick() 槽。

扩展示例:多对一连接
添加另一个信号,连接到同一槽。修改 mainwindow.h 添加 void anotherSignal();,在构造函数连接,并从另一个按钮发射。

五、常见问题和调试

  • 未使用 Q_OBJECT 宏:信号与槽机制依赖于 Qt 的元对象系统,必须在类中加入 Q_OBJECT 宏。问题:编译通过但运行时信号不触发。调试:检查类是否继承 QObject,并添加 Q_OBJECT。
  • 信号未正确连接:确保 connect() 函数的参数类型匹配。问题:信号发射但槽不调用。调试:检查 connect() 返回值,如果 false,打印 QMetaObject::connectFailed 错误。
  • 对象未正确管理:确保信号发出者和槽接收者对象的生命周期被正确管理,避免提前销毁。问题:悬垂指针导致崩溃。调试:使用 QObject::destroyed 信号监控。
  • 扩展问题:跨线程信号延迟。调试:使用 Qt::DirectConnection 测试同步行为;启用 Qt 日志(qDebug() << “Signal emitted”;)。
  • 线程问题:默认连接是 Qt::AutoConnection,在不同线程中自动切换为 Queued。问题:UI 线程阻塞。调试:使用 QMetaObject::invokeMethod() 显式调用。

调试工具:Qt Creator 内置调试器(F5 运行),设置断点在槽函数;使用 qInstallMessageHandler 捕获 Qt 警告。

六、带参数的信号和槽

上面的实例中信号和槽是没有带参数传递的,那么信号和槽可以带参数传递吗?那是肯定的,在 Qt 的信号与槽机制中,信号和槽可以带参数。这种功能非常强大,可以让你在发射信号时传递数据到槽函数中进行处理。接下来,我们将演示如何为信号和槽添加参数。参数类型必须匹配(编译检查),支持默认参数和重载。

修改示例带参数
假设我们要修改前面的示例,使得自定义信号 customSignal 可以传递一个字符串参数,并在槽函数中处理这个参数。

修改 mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>  // 扩展:显式包含

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

signals:
    void customSignal(const QString &message); // 带参数的信号声明
    void valueChanged(int value);  // 扩展:另一个带参数信号

private slots:
    void on_pushButton_clicked();
    void handleCustomSignal(const QString &message); // 带参数的槽声明
    void handleValueChanged(int value);  // 扩展:处理另一个信号

private:
    Ui::MainWindow *ui;
    int counter = 0;  // 扩展:计数器
};

#endif // MAINWINDOW_H

修改 mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 连接自定义信号到槽,注意这里的参数类型需要匹配
    connect(this, &MainWindow::customSignal, this, &MainWindow::handleCustomSignal);
    connect(this, &MainWindow::valueChanged, this, &MainWindow::handleValueChanged);
}

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

void MainWindow::on_pushButton_clicked()
{
    // 修改标签的文本
    ui->label->setText("Button Clicked!");
    // 发射自定义信号,传递参数
    emit customSignal("Custom Signal Emitted with Parameter!");
    emit valueChanged(++counter);  // 扩展:发射另一个信号
}

void MainWindow::handleCustomSignal(const QString &message)
{
    // 处理自定义信号,接收参数
    ui->label->setText(message);
}

void MainWindow::handleValueChanged(int value)
{
    // 扩展:更新状态标签
    ui->statusLabel->setText(QString("Counter: %1").arg(value));
}

运行程序
点击 Qt Creator 工具栏上的 “Run” 按钮。
在弹出的窗口中,点击按钮,观察标签的文本变化。

详细解释
信号声明:在类中使用 signals 关键字声明带参数的信号。例:void customSignal(const QString &message);
槽声明和定义:在类中用 private slots 或 public slots 声明带参数的槽,并实现槽函数。例:void handleCustomSignal(const QString &message);
连接信号和槽:使用 QObject::connect() 函数将带参数的信号和槽连接起来。注意信号和槽的参数类型必须匹配。
发射信号:在适当时机调用信号,并传递参数。例:emit customSignal(“Custom Signal Emitted with Parameter!”);
处理参数:在槽函数中接收并处理传递的参数。

测试代码与输出
多次点击按钮,观察 counter 增加,标签更新。测试:参数类型不匹配(编译错误);默认参数(信号不支持,默认在槽中)。

扩展示例:多参数信号
添加 void multiParamSignal(int a, double b);,连接并发射,槽中计算 a + b。

七、信号(signals)的访问权限

上面的例子中,我们看到信号的声明

signals:
    void customSignal(const QString &message); // 带参数的信号声明

并没有带public、protected 或 private关键字,那么它到底是什么权限呢?下面我们进行讲解。

在 Qt 中,信号(signals)在类中是默认的 protected 访问权限,而不是 private。这意味着信号通常只能从类内部或其派生类中发射(emit)。然而,在实际使用中,你可以将信号声明为 public、protected 或 private,以控制其访问权限。默认 protected 确保信号不被外部随意发射,防止误用。

默认情况
如果你不显式声明信号的访问权限,信号默认是 protected:

signals:
    void customSignal(const QString &message); // 带参数的信号,默认是protected

显式声明访问权限
你可以显式地声明信号为 public、protected 或 private:

如 public 信号:

示例

如果我们希望信号能够从类的外部发射,可以将信号声明为 public

修改 mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

public signals: // 显式声明信号为public
    void customSignal(const QString &message); // 带参数的信号声明

private slots:
    void on_pushButton_clicked();
    void handleCustomSignal(const QString &message); // 带参数的槽声明

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

修改 mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 连接自定义信号到槽,注意这里的参数类型需要匹配
    connect(this, &MainWindow::customSignal, this, &MainWindow::handleCustomSignal);
}

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

void MainWindow::on_pushButton_clicked()
{
    // 修改标签的文本
    ui->label->setText("Button Clicked!");
    // 发射自定义信号,传递参数
    emit customSignal("Custom Signal Emitted with Parameter!");
}

void MainWindow::handleCustomSignal(const QString &message)
{
    // 处理自定义信号,接收参数
    ui->label->setText(message);
}

关键说明
默认情况下,signals 关键字下声明的信号是 protected。
你可以根据需要显式地将信号声明为 public、protected 或 private。
选择合适的访问权限取决于你希望信号从哪里发射和使用。

扩展解释

  • public signals:允许外部发射,适合 API 设计。
  • private signals:仅本类内部发射,增强封装。
  • 潜在问题:public 信号易被滥用,导致调试困难;建议默认 protected。
  • 测试:派生类继承 MainWindow,尝试从外部发射 protected 信号(编译错误)。

八、信号和槽的优点

  • 松耦合:信号和槽使对象之间的通信更加松散,无需对象彼此了解。扩展:在一个模块化 GUI 中,按钮无需知道标签的具体类。
  • 类型安全:编译时检查信号和槽的签名是否匹配,避免了运行时错误。扩展:参数类型、数量、const 均检查。
  • 灵活性:可以动态连接和断开信号和槽,支持多种连接模式(例如,一个信号连接多个槽,或多个信号连接一个槽)。扩展:运行时使用 QMetaObject::connectSlotsByName() 自动连接(基于对象名)。
  • 扩展优点:支持 lambda 作为槽(C++11+),简化匿名处理;多线程友好;易于单元测试(mock 信号发射)。

潜在缺点:轻微性能开销;调试复杂(使用 Qt Signal Spy 测试信号发射)。

总结

Qt 的信号和槽机制为开发者提供了一种优雅、灵活且类型安全的方式来处理对象间的通信。通过理解和利用这一机制,可以显著提高应用程序的模块化、可维护性和可扩展性。扩展建议:实践大型项目,如聊天应用(信号用于消息通知);阅读 Qt 源码(QObject::connectImpl)深入理解;使用 Qt 助手文档查询高级用法(如 Qt::BlockingQueuedConnection)。

以下是为 Qt 信号与槽教程添加的 更多代码示例,覆盖常见场景和进阶用法。每个示例都包含:

  • 完整可运行代码(假设 Qt Widgets 项目)
  • 简要说明
  • 关键点解释
  • 运行后预期行为 / 测试建议

你可以直接复制到 Qt Creator 的新项目中测试(记得在 .pro 文件中包含 QT += widgets)。

示例 1:一个信号连接多个槽(一对多)

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

signals:
    void valueChanged(int newValue);

private slots:
    void updateLabel1(int val);
    void updateLabel2(int val);
    void updateStatusBar(int val);
    void on_pushButton_clicked();

private:
    Ui::MainWindow *ui;
    int counter = 0;
};

#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 一个信号连接三个槽
    connect(this, &MainWindow::valueChanged, this, &MainWindow::updateLabel1);
    connect(this, &MainWindow::valueChanged, this, &MainWindow::updateLabel2);
    connect(this, &MainWindow::valueChanged, this, &MainWindow::updateStatusBar);
}

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

void MainWindow::on_pushButton_clicked()
{
    counter++;
    emit valueChanged(counter);
}

void MainWindow::updateLabel1(int val)
{
    ui->label1->setText(QString("Label 1: %1").arg(val));
}

void MainWindow::updateLabel2(int val)
{
    ui->label2->setText(QString("Label 2: %1").arg(val * 10));
}

void MainWindow::updateStatusBar(int val)
{
    statusBar()->showMessage(QString("Counter updated to %1").arg(val), 3000);
}

说明

  • 一个按钮点击 → 发出 valueChanged 信号
  • 三个槽同时响应:更新两个标签 + 状态栏消息
  • 演示了信号的广播能力

测试

  • 在 UI 中放置 pushButton、label1、label2
  • 每次点击按钮,两个标签显示不同计算结果,状态栏短暂显示消息

示例 2:使用 lambda 作为槽(C++11+ 现代写法)

// mainwindow.cpp 片段(构造函数内)
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 使用 lambda 连接信号
    connect(ui->pushButton, &QPushButton::clicked, this, [=]() {
        static int count = 0;
        ui->label->setText(QString("Clicked %1 times").arg(++count));
        
        // 捕获 this 的例子
        if (count >= 5) {
            ui->pushButton->setEnabled(false);
            ui->label->setStyleSheet("color: red; font-weight: bold;");
        }
    });

    // 带参数的 lambda
    connect(ui->spinBox, QOverload<int>::of(&QSpinBox::valueChanged),
            this, [=](int value) {
        ui->progressBar->setValue(value);
        ui->label->setText(QString("Value: %1").arg(value));
    });
}

说明

  • lambda 让槽函数可以写在连接处,代码更紧凑
  • [=] 捕获所有变量(值捕获),适合简单场景
  • QOverload<int>::of 用于解决重载函数指针歧义

测试

  • 放置 QPushButton、QLabel、QSpinBox、QProgressBar
  • 点击按钮 5 次后按钮禁用,文字变红
  • 调节 spinbox,进度条和标签同步变化

示例 3:跨线程信号与槽(QueuedConnection)

// worker.h
#ifndef WORKER_H
#define WORKER_H

#include <QObject>
#include <QThread>

class Worker : public QObject
{
    Q_OBJECT

public:
    explicit Worker(QObject *parent = nullptr) : QObject(parent) {}

public slots:
    void doWork()
    {
        for (int i = 1; i <= 10; ++i) {
            QThread::msleep(300);
            emit progress(i * 10);  // 发出进度信号
        }
        emit finished();
    }

signals:
    void progress(int percentage);
    void finished();
};

#endif // WORKER_H
// mainwindow.cpp 片段
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    Worker *worker = new Worker;
    QThread *thread = new QThread(this);

    worker->moveToThread(thread);

    // 重要:使用 QueuedConnection 确保跨线程安全
    connect(thread, &QThread::started, worker, &Worker::doWork);
    connect(worker, &Worker::progress, this, &MainWindow::updateProgress);
    connect(worker, &Worker::finished, thread, &QThread::quit);
    connect(worker, &Worker::finished, worker, &Worker::deleteLater);
    connect(thread, &QThread::finished, thread, &QThread::deleteLater);

    connect(ui->startButton, &QPushButton::clicked, thread, &QThread::start);
}

void MainWindow::updateProgress(int percent)
{
    ui->progressBar->setValue(percent);
    ui->label->setText(QString("Progress: %1%").arg(percent));
}

说明

  • Worker 在子线程执行耗时任务
  • 信号通过事件队列跨线程传递到主线程更新 UI(线程安全)
  • 正确清理:使用 deleteLater() 避免直接 delete 跨线程对象

测试

  • 点击 startButton,进度条缓慢增加到 100%,期间主界面不卡顿

示例 4:断开连接 + 动态连接管理

private:
    QMetaObject::Connection connectionHandle;

private slots:
    void toggleConnection()
    {
        if (connectionHandle) {
            QObject::disconnect(connectionHandle);
            connectionHandle = {};
            ui->label->setText("Connection disconnected");
            ui->toggleButton->setText("Reconnect");
        } else {
            connectionHandle = connect(ui->pushButton, &QPushButton::clicked,
                                       this, &MainWindow::on_pushButton_clicked);
            ui->label->setText("Connection re-established");
            ui->toggleButton->setText("Disconnect");
        }
    }

说明

  • 保存 connect() 返回的 Connection 对象
  • 可随时断开或重新连接
  • 常用于开关功能、临时禁用事件等场景

测试

  • 放置 toggleButton 和 pushButton
  • 点击 toggle 切换连接状态,只有连接时点击 pushButton 才会触发更新

示例 5:使用 QSignalSpy 进行单元测试(高级)

// 测试代码(单独的测试项目中使用 QtTest)
#include <QtTest>
#include "mainwindow.h"

class tst_SignalSlot : public QObject
{
    Q_OBJECT

private slots:
    void test_button_click_emits_signal()
    {
        MainWindow w;
        QSignalSpy spy(&w, &MainWindow::valueChanged);

        QCOMPARE(spy.count(), 0);

        // 模拟点击
        QMetaObject::invokeMethod(w.findChild<QPushButton*>("pushButton"),
                                  "clicked", Qt::DirectConnection);

        QCOMPARE(spy.count(), 1);
        QCOMPARE(spy.takeFirst().at(0).toInt(), 1);
    }
};

说明

  • QSignalSpy 可以捕获信号参数,用于自动化测试
  • 常用于 CI/CD 中的单元测试

希望这些示例能覆盖你大部分实际开发场景!
如果你想要特定场景的更多示例(例如:

  • 多个窗口间通信
  • 自定义控件发出信号
  • 信号与 QTimer / QNetworkReply 结合
  • 阻塞式 vs 非阻塞式连接对比
  • 性能测试对比),请告诉我具体方向,我可以继续补充。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐