文章

信号和槽简介

信号和槽是 Qt 的事件机制,用于对象间通信。一个对象发出信号,另一个对象的槽函数自动响应。

信号和槽简介

信号和槽简介

Qt 的 信号(Signal)与槽(Slot)机制 是其最核心的特性之一,用于实现对象之间的事件驱动式通信,特别适用于 GUI 开发中控件响应用户操作的场景。它是一种比回调函数更安全、解耦性更高的通信方式。

信号

信号(Signal)是什么

信号是对象在特定事件发生时“发出的通知”,用于告诉其他对象“某事发生了”。

特点:

  • Qt 元对象系统(Meta-Object System) 的一部分。
  • 通常是类的一部分(用 signals: 声明),但不是函数实现。
  • 可以传递参数,支持重载。
  • 不需要手动调用,常通过控件内部事件自动触发。
  • 只能由该类或其子类通过 emit 发出。

信号的声明语法

1
2
3
signals:
    void signalName();              // 无参数信号
    void signalWithArgs(int value, QString name);  // 有参数信号

注意:信号没有函数体!它们只是声明,不需要定义

触发信号:使用 emit 关键字

在类中,可以在需要时调用 emit 发射信号:

1
emit signalWithArgs(42, "example");

使用信号的前提条件

  • 类必须继承自 QObject

  • 类中必须包含 Q_OBJECT 宏(启用元对象功能)。

什么是槽(slot)

槽是一个普通的成员函数,但它可以连接(connect)到一个信号(signal),当信号被发射时,Qt 自动调用相应的槽函数。

语法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <QPushButton>
#include <QObject>

class MyObject : public QObject {
    Q_OBJECT  // 必须宏,启用 Qt 的元对象机制

public slots:  // 标识该区域内的函数为槽函数
    void onButtonClicked() {
        qDebug("Button was clicked!");
    }
};

// 连接信号和槽
QPushButton* button = new QPushButton("Click me");
MyObject* obj = new MyObject;
QObject::connect(button, &QPushButton::clicked, obj, &MyObject::onButtonClicked);

slot 是做什么的

  • Q_OBJECT 宏支持下,slots 关键字用于声明函数为槽。

  • 本质上,它会让 Qt 的元对象编译器(moc)生成额外的代码,以支持运行时的动态连接。

  • 只有标记为 slot 的函数才能被信号连接调用,且能被元对象系统识别。

slots 的写法方式

1
2
3
4
5
6
public slots:
    void foo();   // 公有槽
protected slots:
    void bar();   // 保护槽
private slots:
    void baz();   // 私有槽

这些和访问权限修饰符一样作用于槽函数。

信号和槽的连接方式

Qt 中信号与槽机制有两种连接方式:老式字符串宏方式现代函数指针方式

1
2
3
4
// 宏方式
connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(FoodComing()));
// 函数指针方式
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::FBIComing);

对比:

比较项宏方式 (SIGNAL, SLOT)函数指针方式
类型检查❌ 无编译期检查✅ 编译期检查
lambda 支持❌ 不支持✅ 支持
槽是否必须加 slots✅ 是(否则无法注册)❌ 否(普通成员函数即可)
Qt 版本支持Qt 4 和 Qt 5,Qt 6 已不推荐Qt 5+(特别推荐),Qt 6 默认

示例 1:手动关联

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT	// 必须宏,启用 Qt 的元对象机制

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

public slots:	// 标识该区域内的函数为槽函数
    void FBIComing();

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "widget.h"
#include "./ui_widget.h"
#include <QMessageBox>

Widget::Widget(QWidget* parent) : QWidget(parent), ui(new Ui::Widget) {
    ui->setupUi(this);
    // 连接信号和槽
    // connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(FoodComing()));
    connect(ui->pushButton, &QPushButton::clicked, this, &Widget::FBIComing);
}

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

void Widget::FBIComing() {
    QMessageBox::information(this, tr("查水表"), tr("FBI!Open the door!"));
}

&QPushButton::clickedC++11 引入的指向成员函数的指针写法,在 Qt 中用于实现类型安全的信号与槽连接,它本质上是一个「指向 QPushButton 的成员函数 clicked」的函数指针。

1
2
// 信号的函数指针类型:
void (QPushButton::*clickedSignal)(bool) = &QPushButton::clicked;

这表示:clicked 是 QPushButton 的一个成员函数,返回类型为 void,参数为 bool

示例 2:自动关联

对于之前的例子,先删除 void FBIComing()widget.h 中的声明,在 widget.cpp 中的定义以及手动关联的代码。然后打开界面文件 widget.ui ,进入图形界面设计模式,右击 “ 敲门 ” 按钮,转到槽 -> 选择信号 -> QAbstractButton -> clicked(),点击确认后会自动生成一个 void Widget::on_pushButton_clicked() 函数声明和定义,函数定义中填写之前的代码。

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

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

// 自动生成
private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "widget.h"
#include "./ui_widget.h"
#include <QMessageBox>

Widget::Widget(QWidget* parent) : QWidget(parent), ui(new Ui::Widget) {
    ui->setupUi(this);
    // 没有手动关联的代码
}

Widget::~Widget() {
    delete ui;
}
// 自动生成
void Widget::on_pushButton_clicked() {
    // 自己填写
    QMessageBox::information(this, tr("查水表"), tr("FBI!Open the door!"));
}

没有手动关联的 connect 函数,信号和槽是如何工作的?

Qt 使用一种固定的命名约定来自动将信号连接到槽函数,命名格式如下:

1
on_<objectName>_<signalName>()
1
on_<objectName>_<signalName>([参数列表])

示例中的是:

1
on_pushButton_clicked()

只要在代码里写出了这个函数(并且有 Q_OBJECT 宏),Qt 的元对象机制会自动帮你连接这个信号。所以就不用手动写 connect 函数。如果不通过 widget.ui 右键点击自动创建槽函数的方式,也可以直接在代码里写,只要严格按照命名规则,也可以实现自动关联的功能。

注意事项

限制描述
必须启用 Q_OBJECT否则元对象机制无效
objectName 必须准确比如 lineEdit_1pushButton_OK
不支持 lambda 或重载自动连接只能匹配完整信号名
需要在构造函数之后才能自动连上自动连接是在构造函数之后进行的
不推荐用于复杂逻辑或大型项目可读性差、调试困难

示例 3:文本同步(使用原有的信号和槽)

使用拖控件的方式创建一个单行文本编辑器(QLineEdit)和一个标签控件(QLabel),然后用垂直布局包裹这两个控件。

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "widget.h"
#include "./ui_widget.h"

Widget::Widget(QWidget* parent) : QWidget(parent), ui(new Ui::Widget) {
    ui->setupUi(this);
    // 函数指针写法中不能加上参数列表 ()
    connect(ui->lineEdit, &QLineEdit::textEdited, ui->label, &QLabel::setText);
}

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

QLineEdit 自带的信号:

1
void QLineEdit::textChanged(const QString &text)

QLabel 自带的槽:

1
void setText(const QString &)

错误写法:

1
&QLineEdit::textEdited(QString)   错误:这不是合法的 C++ 函数指针写法
  • 在函数指针语法中,不能加括号和参数列表,比如 &Class::functionName 就足够了;

  • 参数信息是由编译器通过模板推导自动判断的。

示例 4:一对多关联

打开 widget.ui 向垂直布局里增加一个文本浏览控件(QTextBrowser),然后自定义一个槽函数。

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

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

public slots:
    // 自定义的槽函数
    void printText(const QString& text);

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "widget.h"
#include "./ui_widget.h"

Widget::Widget(QWidget* parent) : QWidget(parent), ui(new Ui::Widget) {
    ui->setupUi(this);
    // 接收端是标签控件
    connect(ui->lineEdit, &QLineEdit::textEdited, ui->label, &QLabel::setText);
    // 接收端是文本浏览控件
    connect(ui->lineEdit, &QLineEdit::textEdited, ui->textBrowser, &QTextBrowser::setText);
    // 接收端是主窗口的 printText 槽
    connect(ui->lineEdit, &QLineEdit::textEdited, this, &Widget::printText);
    // 接收端是主窗口内的一个 lambda 表达式,用于打印文本
    // 只有在使用 lambda 表达式时才能省略接收者
    connect(ui->lineEdit, &QLineEdit::textEdited,
            [](const QString& text) { qDebug() << "printText2: " << text; });
}

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

void Widget::printText(const QString& text) {
    // 打印到调试输出面板
    qDebug() << "printText: " << text;
}

示例 5:多对一关联

widget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

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

public slots:
    void someoneComing();

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H

widget.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "widget.h"
#include "./ui_widget.h"
#include <QMessageBox>

Widget::Widget(QWidget* parent) : QWidget(parent), ui(new Ui::Widget) {
    ui->setupUi(this);
    // 三个按钮都关联到同一个槽函数,省略第三个参数 this
    connect(ui->fbiBtn, &QPushButton::clicked, this, &Widget::someoneComing);
    connect(ui->ciaBtn, &QPushButton::clicked, this, &Widget::someoneComing);
    connect(ui->deaBtn, &QPushButton::clicked, this, &Widget::someoneComing);
}

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

void Widget::someoneComing(){
    QString name = this->sender()->objectName();
    qDebug() << name;

    QString msg = "我去!是";
    if("fbiBtn" == name){
        msg.append("FBI!");
    }else if("ciaBtn" == name){
        msg.append("CIA!");
    }else if("deaBtn" == name){
        msg.append("DEA!");
    }else{
        return;
    }

    QMessageBox::information(this, tr("有人敲门"), msg);
}

在 Qt 中,当信号触发其关联的槽函数时,槽函数可以通过 sender() 获取发出该信号的对象指针。这个 sender() 函数是继承自 QObject 的一个成员函数,返回类型是 QObject*

1
QObject* QObject::sender() const;
  • sender() 仅在槽函数是在事件调用链中由信号触发时才有效,否则返回 nullptr

  • sender() 只能在同一个线程中被安全调用,跨线程信号槽连接(使用 Qt::QueuedConnection)时,结果可能不确定。

  • 返回的是 QObject*,如果你想调用派生类函数,需要手动 qobject_cast

1
2
3
if (auto btn = qobject_cast<QPushButton*>(sender())) {
    btn->setText("已点击");
}

什么时候 sender() 不靠谱?

  • 槽函数是手动调用的(而不是 connect 后自动触发的);

  • 槽函数是由不同线程中的信号触发的;

  • 使用 lambda 表达式捕获 sender() 时,由于作用域不同,也不建议依赖它。

示例 6:解除关联

理论

1. 精确解除某个信号和槽的连接
1
disconnect(sender, signal, receiver, slot);

对应 connect 的写法是:

1
connect(ui->button, &QPushButton::clicked, this, &Widget::onButtonClicked);

解除方式:

1
disconnect(ui->button, &QPushButton::clicked, this, &Widget::onButtonClicked);
2. 只写发送者和信号,断开该信号的所有连接
1
disconnect(ui->button, &QPushButton::clicked, nullptr, nullptr);

表示断开 ui->button 所有发出的 clicked 信号连接。

3. 只写接收者和槽,断开接收者中对应槽的所有连接
1
disconnect(nullptr, nullptr, this, &Widget::onButtonClicked);
4. 断开发送者和接收者之间的所有连接
1
disconnect(ui->button, nullptr, this, nullptr);
5. 对 lambda 表达式的连接,断开比较麻烦
1
2
3
4
5
6
7
// 连接
auto connection = connect(ui->button, &QPushButton::clicked, [](){
    qDebug() << "lambda";
});

// 断开
disconnect(connection);
补充说明
  • 所有 disconnect(...) 的重载都在 QObject 类中;

  • 也可以使用 QObject::disconnect(sender) 断开所有与某个对象相关的连接;

  • disconnect() 返回 bool,指示是否成功解除连接。

推荐实践
  • 使用 QMetaObject::Connection conn = connect(...) 保存连接对象;

  • 在需要时使用 disconnect(conn) 断开。

实践

创建一个 QLineEdit,一个 QLabel,两个 QPushButton,自动创建出两个槽函数。

widget.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget {
    Q_OBJECT

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

private slots:
    void on_connBtn_clicked();

    void on_disconnBtn_clicked();

private:
    Ui::Widget* ui;
};
#endif // WIDGET_H
widget.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "widget.h"
#include "./ui_widget.h"

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

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

void Widget::on_connBtn_clicked() {
    // 关联
    connect(ui->lineEdit, &QLineEdit::textEdited, ui->label, &QLabel::setText);

    ui->connBtn->setEnabled(false);
    ui->disconnBtn->setEnabled(true);
}

void Widget::on_disconnBtn_clicked() {
    // 断开关联
    disconnect(ui->lineEdit, &QLineEdit::textEdited, ui->label, &QLabel::setText);

    ui->connBtn->setEnabled(true);
    ui->disconnBtn->setEnabled(false);
}
本文由作者按照 CC BY 4.0 进行授权