信号和槽简介
信号和槽是 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::clicked
是 C++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_1 、pushButton_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);
}