信号槽是 Qt 框架引以为豪的机制之一,实际体现的是观察者模式(发布-订阅模式)。当某个事件发生后(如按钮被点击),它会发出一个信号(signal),任何接收方对象只要使用 connect()
将其槽函数绑定上,就可以自动触发槽(slot)函数进行处理。
就像广播一样,信号发出后不指定接收者,而是由接收者决定要不要处理这个信号。
信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时候Qt对应的窗口类会发出某个信号,以此对用户的挑选做出反应。
信号 = 事件的通知机制。常见事件如:
按钮单击、双击
鼠标移动、按下、释放
键盘输入
窗口刷新
那么在Qt中信号是通过什么形式呈现给使用者的呢?
信号的本质是函数(只有声明没有定义),由某个实例化对象在检测到事件后自动调用,通知接收者。
槽函数 = 对信号的响应处理,是类的普通成员函数或静态函数。可以通过 connect()
和信号连接。
[!NOTE]
在Qt中槽函数是一类特殊的功能的函数,在编码过程中也可以作为类的普通成员函数来使用。之所以称之为槽函数是因为它们还有一个职责就是对Qt框架中产生的信号进行处理。
示例类比:
实例对象 | 角色 | 描述 |
---|---|---|
女朋友 | 信号发出者 | 说“我饿了!” |
我 | 信号接收者 | 听到信号 → 处理:带她去吃饭 |
在Qt中槽函数的所有者也是某个类的实例对象。 |
在Qt中信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求我们可以将二者连接到一起:
信号和槽是独立的个体,需要通过 QObject::connect()
函数建立连接。
QMetaObject::Connection QObject::connect(
const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection);
参数:
- sender: 发出信号的对象
- signal: 属于sender对象, 信号是一个函数, 这个参数的类型是函数
指针, 信号函数地址
- receiver: 信号接收者
- method: 属于receiver对象, 当检测到sender发出了signal信号,
receiver对象调用method方法,信号发出之后的处理动作
// 参数 signal 和 method 都是函数地址, 因此简化之后的 connect() 如下:
connect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::method);
⚠️ 注意事项:
connect()
注册的是事件处理动作,并不立即触发。
信号发出后 Qt 框架自动调用对应的槽函数。
sender 和 receiver 必须已实例化,否则连接无效。
Qt 的标准控件(如按钮、窗口等)都包含内置信号和槽函数,例如:
QPushButton
单击信号其实定义在其父类 QAbstractButton
中。查找方式:
signals
和 slots
项功能:点击按钮 → 关闭窗口
元素 | 角色 | 类型 |
---|---|---|
按钮 | 信号源 | QPushButton |
窗口 | 信号接收 | QWidget |
功能实现: 点击窗口上的按钮, 关闭窗口
功能分析:
- 按钮: 信号发出者 -> QPushButton 类型
- 窗口: 信号的接收者和处理者 -> QWidget 类型
// 单击按钮发出的信号
[signal] void QAbstractButton::clicked(bool checked = false)
// 关闭窗口的槽函数
[slot] bool QWidget::close();
connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);
通常在构造函数中调用
connect()
,事件注册先行,事件触发时再执行处理。
Qt框架提供的信号槽在某些特定场景下是无法满足我们的项目需求的,因此我们还设计自己需要的的信号和槽,同样还是使用connect()对自定义的信号槽进行连接。
如果想要在QT类中自定义信号槽, 需要满足一些条件, 并且有些事项也需要注意:
要编写新的类并且让其继承Qt的某些标准类
这个新的子类必须从QObject类或者是QObject子类进行派生
在定义类的头文件中加入 Q_OBJECT 宏
// 在头文件派生类的时候,首先像下面那样引入Q_OBJECT宏:
class MyMainWindow : public QWidget
{
Q_OBJECT
......
}
在Qt中信号的本质是事件, 但是在框架中也是以函数的形式存在的, 只不过信号对应的函数只有声明, 没有定义。如果Qt中的标准信号不能满足我们的需求,可以在程序中进行信号的自定义,当自定义信号对应的事件产生之后,认为的将这个信号发射出去即可(其实就是调用一下这个信号函数)。
[!important]
下边给大家阐述一下, 自定义信号的要求和注意事项:
信号是类的成员函数
返回值必须是 void 类型
信号的名字可以根据实际情况进行指定
参数可以随意指定, 信号也支持重载
信号需要使用 signals 关键字进行声明, 使用方法类似于public等关键字
信号函数只需要声明, 不需要定义(没有函数体实现)
在程序中发射自定义信号: 发送信号的本质就是调用信号函数
习惯性在信号函数前加关键字: emit, 但是可以省略不写
emit只是显示的声明一下信号要被发射了, 没有特殊含义
底层 emit ==
#define emit
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class Test : public QObject
{
Q_OBJECT
signals:
void testsignal();
// 参数的作用是数据传递, 谁调用信号函数谁就指定实参
// 实参最终会被传递给槽函数
void testsignal(int a);
};
槽函数就是信号的处理动作,在Qt中槽函数可以作为普通的成员函数来使用。如果标准槽函数提供的功能满足不了需求,可以自己定义槽函数进行某些特殊功能的实现。自定义槽函数和自定义的普通函数写法是一样的。
[!important]
下边给大家阐述一下, 自定义槽的要求和注意事项:
- 返回值必须是 void 类型
- 槽也是函数, 因此也支持重载
- 槽函数需要指定多少个参数, 需要看连接的信号的参数个数
- 槽函数的参数是用来接收信号传递的数据的, 信号传递的数据就是信号的参数
信号函数: void testsig(int a, double b);
槽函数: void testslot(int a, double b);
总结:
// 信号函数:
void testsig(int a, double b);
// 槽函数:
void testslot(int a);
Qt中槽函数的类型是多样的:
public slots:
private slots: // 这样的槽函数不能在类外部被调用
protected slots: // 这样的槽函数不能在类外部被调用
// 槽函数书写格式举例
// 类中的这三个函数都可以作为槽函数来使用
class Test : public QObject
{
public:
void testSlot();
static void testFunc();
public slots:
void testSlot(int id);
};
还是上边的场景: 女朋友说:“我肚子饿了!”,于是我带她去吃饭。
// 定义一个 GirlFriend 类,继承自 QObject(必须,才能使用 Qt 的信号槽机制)
class GirlFriend : public QObject
{
Q_OBJECT // Qt 的元对象宏,启用信号槽机制(必须写)
public:
// 构造函数,带一个父对象参数,默认值为 nullptr
// 父对象机制是 Qt 内存管理的一部分,用于对象生命周期管理
explicit GirlFriend(QObject *parent = nullptr);
signals:
// 声明一个不带参数的信号 hungry(信号是函数声明,没有函数体)
// 当这个信号被触发时,只能表达“饿了”,但无法说明具体想吃什么
void hungry();
// 声明一个带参数的信号 hungry,参数类型为 QString
// 参数 msg 可以传递想吃什么(如“烧烤”、“火锅”等)
void hungry(QString msg);
};
// 定义一个 Me 类,继承自 QObject,用于处理接收到的 hungry 信号
class Me : public QObject
{
Q_OBJECT // 启用 Qt 的信号槽系统
public:
// 构造函数,带一个父对象参数
explicit Me(QObject *parent = nullptr);
public slots:
// 槽函数:eatMeal,不带参数
// 接收不带参数的 hungry 信号,只能响应“吃饭”动作,但不知道具体吃什么
void eatMeal();
// 槽函数:eatMeal,带一个 QString 类型参数
// 接收带参数的 hungry 信号,可以根据 msg 内容决定吃什么
void eatMeal(QString msg);
};
QObject
QObject
是 Qt 所有对象模型功能(如信号槽、对象树、动态属性、事件机制等)的基础类。
QObject
是 Qt 所有对象类的父类几乎所有重要的 Qt 类都继承自 QObject
,如 QWidget
、QPushButton
、QTimer
、QThread
、QApplication
等。
只有继承自 QObject
的类才能使用 signals
和 slots
关键字。
QObject
搭配 Q_OBJECT
宏,可以启用 元对象系统(Meta-Object System),这使得 connect()
和 emit
成为可能。
QObject
都可以有一个父对象(QObject *parent
)QPushButton *btn = new QPushButton(parentWidget); // parentWidget 是父对象
返回对象指针给btn
QObject::event()
分发的。特性 | 是否由 QObject 提供 | 说明 |
---|---|---|
信号槽机制 | ✅ | 必须继承 QObject,且加上 Q_OBJECT 宏 |
对象名管理 | ✅ | objectName() 、setObjectName() |
父子对象管理 | ✅ | 自动管理内存,层级结构清晰 |
事件处理 | ✅ | 所有 QObject 子类都能接收和处理事件 |
动态属性 | ✅ | setProperty() 、property() |
定时器 | ✅ | startTimer() 、timerEvent() |
class GirlFriend : public QObject
{
Q_OBJECT // 启用 Qt 元对象系统,才能使用信号槽
public:
explicit GirlFriend(QObject *parent = nullptr); // 支持父对象管理
signals:
void hungry(); // 只有继承 QObject 的类才能使用 signals 关键字
};
class MyClass : public QObject
{
Q_OBJECT
};
Q_OBJECT
是启用元对象特性的关键(如信号槽、反射机制等)。QObject
,信号槽也不能工作。[!NOTE] Qt 类继承结构简图:
QObject ├── QPaintDevice │ ├── QWidget │ ├── QPushButton │ ├── QLabel │ └── ... ├── QThread ├── QTimer ├── QApplication └── ...
connect()
连接connect()
函数的调用顺序没有关系connect()
连接connect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::siganl-new);
disconnect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::method);
// 语法:
QMetaObject::Connection QObject::connect(
const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection);
// 参数5:type(连接方式,可选参数,默认 Qt::AutoConnection)
// 表示连接的类型,共有以下几种枚举值:
// Qt::AutoConnection:自动判断线程,是否异步处理(默认值)
// Qt::DirectConnection:同步执行槽函数(通常用于同线程)
// Qt::QueuedConnection:异步执行槽函数(通常用于不同线程)
// Qt::BlockingQueuedConnection:异步 + 等待完成(用于特殊场景)
// Qt::UniqueConnection:防止重复连接同一个信号和槽(常用于初始化中)
信号和槽函数也就是第2,4个参数传递的是地址, 编译器在编译过程中会对数据的正确性进行检测
connect(const QObject *sender, &QObject::signal, const QObject *receiver, &QObject::method);
connect(button, &QPushButton::clicked, this, &MainWindow::close);
等价于:
QMetaObject::Connection conn = QObject::connect(
button, // 信号的发送者
&QPushButton::clicked, // 信号的函数地址
this, // 槽函数的接收者
&MainWindow::close, // 槽函数地址
Qt::AutoConnection // 连接方式(默认值) );
这种旧的信号槽连接方式在 Qt5 中是支持的, 但是不推荐使用, 因为这种方式在进行信号槽连接的时候, 信号槽函数通过宏 SIGNAL()
和 SLOT()
转换为字符串类型。
因为信号槽函数的转换是通过宏来进行转换的,因此传递到宏函数内部的数据不会被进行检测, 如果使用者传错了数据,编译器也不会报错,但实际上信号槽的连接已经不对了,只有在程序运行起来之后才能发现问题,而且问题不容易被定位。
// Qt4的信号槽连接方式
[static] QMetaObject::Connection QObject::connect(
const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type = Qt::AutoConnection);
connect(const QObject *sender, SIGNAL(信号函数名(参数1, 参数2, ...)),
const QObject *receiver, SLOT(槽函数名(参数1, 参数2, ...)));
Qt4中声明槽函数必须要使用 slots
关键字, 不能省略。
场景描述:
- 我肚子饿了, 我要吃东西。
分析:
- 信号的发出者是我自己, 信号的接收者也是我自己
我们首先定义出一个Qt的类。
class Me : public QObject
{
Q_OBJECT
public slots: // Qt4中的槽函数必须这样声明, qt5中的关键字 slots 可以被省略
void eat();
void eat(QString somthing);
signals:
void hungury();
void hungury(QString somthing);
};
处理如下逻辑: 我饿了, 我要吃东西
分析: 信号的发出者是我自己, 信号的接收者也是我自己
Me m;
// Qt4处理方式
connect(&m, SIGNAL(hungury()), &m, SLOT(eat()));
connect(&m, SIGNAL(hungury(QString)), &m, SLOT(eat(QString)));
// Qt5处理方式
connect(&m, &Me::hungury, &m, &Me::eat); // error
[!warning] Qt5处理方式错误原因分析:
上边的写法之所以错误是因为这个类中信号槽都是重载过的, 信号和槽都是通过函数名去关联函数的地址, 但是这个同名函数对应两块不同的地址, 一个带参, 一个不带参, 因此编译器就不知道去关联哪块地址了, 所以如果我们在这种时候通过以上方式进行信号槽连接, 编译器就会报错。
可以通过定义函数指针的方式指定出函数的具体参数,这样就可以确定函数的具体地址了。
定义函数指针指向重载的某个信号或者槽函数,在connect()函数中将函数指针名字作为实参就可以了。
// 举例:
void (Me::*func1)(QString) = &Me::eat; // func1指向带参的槽函数
void (Me::*func2)() = &Me::hungury; // func2指向不带参的信号
// 定义函数指针指向重载的某一个具体的槽函数地址
void (Me::*myslot)(QString) = &Me::eat;
// 定义函数指针指向重载的某一个具体的信号地址
void (Me::*mysignal)(QString) = &Me::hungury;
// 使用定义的函数指针完成信号槽的连接
connect(&m, mysignal, &m, myslot);
Qt版本 | 特点 | 推荐使用 |
---|---|---|
Qt4 | 使用宏函数,信号槽转换为字符串,无类型检测,易出错 | ❌ |
Qt5 | 使用函数指针,类型安全,编译期可检测,推荐使用 | ✅ |
重载 | Qt4方式不受影响;Qt5中需要使用函数指针 disambiguation 方式处理 | ✅ |
对比项目 | Qt4 宏写法 | Qt5 函数指针写法 |
---|---|---|
写法是否模糊 | ✅ 可以模糊调用,字符串形式 | ❌ 不允许模糊,函数指针必须唯一 |
函数是否可重载 | ✅ 支持重载,靠字符串匹配 | ✅ 支持,但需要手动 disambiguate |
编译期检查 | ❌ 不检查参数是否匹配 | ✅ 会检查参数是否匹配,安全 |
易错性 | ⚠️ 拼错字符串也能编译 | ✅ 编译器能立刻提示错误 |
推荐程度 | ❌ 不推荐(Qt4 的老写法) | ✅ Qt5 及以上推荐 |
函数重载(Function Overloading)是指在同一个作用域中,多个函数的名字相同,但参数不同(数量或类型不同)。
void print(int x); // 打印整数
void print(double y); // 打印小数
void print(QString str); // 打印字符串
上面这三个 print
函数函数名一样,但参数类型不同,这就是函数重载。
Lambda 表达式是 C++11 最重要也是最常用的特性之一,是现代编程语言的一个特点,简洁,提高了代码的效率并且可以使程序更加灵活。Qt 是完全支持 C++ 语法的, 因此在 Qt 中也可以使用 Lambda 表达式。
Lambda 表达式就是一个匿名函数, 语法格式如下:
[capture](params) opt -> ret {body;};
参数说明:
capture
: 捕获列表params
: 参数列表opt
: 函数选项ret
: 返回值类型body
: 函数体捕获列表:捕获一定范围内的变量
捕获形式 | 说明 |
---|---|
[] |
不捕捉任何变量 |
[&] |
捕获外部作用域中所有变量,并作为引用在函数体内使用 |
[=] |
捕获外部作用域中所有变量,并作为副本在函数体内使用 |
[=, &foo] |
按值捕获所有变量,foo 特别按引用捕获 |
[bar] |
仅按值捕获变量 bar |
[&bar] |
仅按引用捕获变量 bar |
[this] |
捕获当前类中的 this 指针,允许访问类成员 |
参数列表: 与普通函数参数列表相同
opt 选项(可选):
mutable
: 允许修改按值捕获的副本变量(不是原变量)exception
: 指定函数抛出异常类型,如 throw();
void
或只有一个 return
时可省略{}
因为 Lambda 表达式是一个匿名函数,因此没有函数声明,直接定义即可。
但如果只定义不调用,函数不会执行。
// 匿名函数的定义,程序执行时此函数不会被调用
[](){
qDebug() << "hello, 我是一个lambda表达式...";
};
int ret = [](int a) -> int
{
return a+1;
}(100); // 100是传递给匿名函数的参数
在 Lambda 表达式的捕获列表([]
)中添加不同的关键字,就可以在函数体中使用外部变量了。
// 在匿名函数外部定义变量
int a = 100, b = 200, c = 300;
[](){
// 打印外部变量的值
qDebug() << "a:" << a << ", b: " << b << ", c:" << c; // error, 不能使用任何外部变量
};
[&](){
qDebug() << "hello, 我是一个lambda表达式...";
qDebug() << "使用引用的方式传递数据: ";
qDebug() << "a+1:" << a++ << ", b+c= " << b+c;
}();
mutable
修改副本[=](int m, int n) mutable {
qDebug() << "hello, 我是一个lambda表达式...";
qDebug() << "使用拷贝的方式传递数据: ";
// 拷贝的外部数据在函数体内部是只读的
// 如果不添加 mutable,不能修改这些只读数据的值
// 添加 mutable 后允许修改这些副本,注意:不会影响外部变量本身
qDebug() << "a+1:" << a++ << ", b+c= " << b+c;
qDebug() << "m+1: " << ++m << ", n: " << n;
}(1, 2);