010-信号和槽

auther: abinng date: 2026-03-21 11:29 createDate:2026-03-21 11:29

信号和槽的连接

上一篇我们学过了 Qt 的元对象系统,就是为了信号和槽做铺垫

信号和槽的作用是什么呢?

实现 Qt 的对象之间通信、解耦

信号和槽的实现:

  • 设计信号、槽函数
  • 关联信号和槽函数
  • 在一个合适的位置发射信号

怎么将信号和槽关联起来呢?

1
2
3
4
// 字符串形式:
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
// 没有接收者形式:
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, Functor functor)

所以,信号和槽其实就是有若干信号和若干处理函数,将某信号和某槽函数连接起来,之后调用信号函数,就会自动触发与之绑定的槽函数。且该连接是可以断开的,并不是死板的绑定

直接看代码,代码中有对应注释,结合上面引入中的讲解,更好理解

简单的信号和槽连接

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
// info.h
#include <QObject>
#include <qtmetamacros.h>

class A : public QObject {
Q_OBJECT
public:
explicit A(QObject *parent = nullptr);
/* 自定义信号函数
* 1. 支持元对象系统
* 2. 访问权限,Qt 给了关键字 signals,用于定义信号函数
* 3. 信号本质就是一个成员函数,只写声明,不需要实现(MOC会自动生成代码)
* 4. 信号的返回值必须是 void ,参数随意
*/
signals:
void a_signal();
};

class B : public QObject {
Q_OBJECT
public:
explicit B(QObject *parent = nullptr);
/* 自定义槽函数
* 1. 支持元对象系统
* 2. 槽函数本质就是一个成员函数,访问权限 public/private/protected + slots
* 3. 槽函数必须有实现
* 4. 槽函数返回值和输入参数必须和信号一致
*/
public slots:
void b_slot();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// info.cpp
#include "info.h"
#include <QDebug>

A::A(QObject *parent) : QObject(parent) {

}

B::B(QObject *parent) : QObject(parent) {

}

void B::b_slot() {
qDebug() << "start b_slot()";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//main.cpp
#include <QApplication>
#include <QObject>
#include <qapplication.h>
#include "info.h"

void test01() {
A a;
B b;
// 1. 采用字符串方式进行信号和槽的链接关系
// 发出者指针,信号函数名称,接收者指针,槽函数名称
// QObject::connect(&a, SIGNAL(a_signal()), &b, SLOT(b_slot()));
// 2. 采用函数指针的方式来实现信号和槽的关系,编译期间会进行成员有效性的检测
QObject::connect(&a, SIGNAL(a_signal()), &b, SLOT(b_slot()));

getchar();
emit a.a_signal(); // 这里的emit,点进去发现也是空,其实就是为了标识一下,知道是发信号的函数调用
}

int main(int argc, char* argv[]) {
test01();
return 0;
}

上面使用了几种方式来连接信号和槽

  1. 字符串形式的 connect 函数,但是这种方法有一个弊端就是,当我们不小心写错了信号函数/槽函数的名称,编译器也不会报错,会导致 Debug 的时候效率很低 (可以去实际测试一下,故意写错函数名称,也不会报错)

    所以现在更推荐的是用函数指针的方式而不是函数名称的方式,这种方式可以进行检查

  2. 函数指针形式的 connect 函数,就是将函数名称部分换成函数指针了

    • 但是这种方式也会有缺点,可以前往下面的查看

信号和槽是同步还是异步

是同步的,我们可以通过下面的代码进行验证

1
2
// info.h
这个文件的内容不变,还是和上面一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// info.cpp
#include "info.h"
#include <QDebug>
#include <chrono>
#include <thread>

A::A(QObject *parent) : QObject(parent) {

}

B::B(QObject *parent) : QObject(parent) {

}

void B::b_slot() {
qDebug() << "start b_slot()";
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
qDebug() << "end b_slot()";
}
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
// main.cpp
#include <QApplication>
#include <QObject>
#include <qapplication.h>
#include "info.h"

void test01() {
A a;
B b;
// 1. 采用字符串方式进行信号和槽的链接关系
// 发出者指针,信号函数名称,接收者指针,槽函数名称
// QObject::connect(&a, SIGNAL(a_signal()), &b, SLOT(b_slot()));
// 2. 采用函数指针的方式来实现信号和槽的关系,编译期间会进行成员有效性的检测
QObject::connect(&a, &A::a_signal, &b, &B::b_slot);

getchar();
emit a.a_signal(); // 这里emit,点进去发现也是空,其实就是为了标识一下,知道是发信号的函数调用
}

int main(int argc, char* argv[]) {
QApplication app(argc, argv);
test01();
qDebug() << "=====================";
return app.exec();
// return 0;
}

如果是异步的,那么运行后应该看到的结果是:

1
2
3
start b_slot()
=====================
end b_slot()

但是实际的结果是:

1
2
3
start b_slot()
end b_slot()
=====================

说明槽函数执行完之前,是不会执行其他代码的

但同时我们也要注意,槽函数不能占用太多时间,不然就会导致 CPU 一直在执行槽函数,对于用户体感来说会出问题。例如用户短时间执行了两次操作,但是第一个操作对应的槽函数需要处理十几秒,那么显然会影响另一个槽函数的执行

函数指针形式的 connect

上面写的都是无参数的,那我们可以试试带参数的信号和槽

1
2
3
4
5
// info.h
- void a_signal();
+ void a_signal(int); // 因为信号函数只需要写声明,所以不写形参名也可以
- void b_slot();
+ void b_slot(int a); // 槽函数需要写实现,所以得带上形参名
1
2
3
4
5
6
7
8
9
10
11
// info.cpp
- void B::b_slot() {
-    qDebug() << "start b_slot()";
-    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
-    qDebug() << "end b_slot()";
- }
+ void B::b_slot(int a) {
+    qDebug() << "start b_slot()" << a;
+    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+    qDebug() << "end b_slot()";
+ }
1
2
3
4
5
6
7
// main.cpp
void test01() {
...
...
- emit a.a_signal();
+ emit a.a_signal(10);
}

之后执行,输出为:

1
2
3
start b_slot() 10
end b_slot()
=====================

看到代码和对应的输出之后,就大概理解了这个参数是怎么在信号和槽之间传递的了

就是 信号无参--槽无参信号有参---槽有参

但是有一个明显的缺点是:函数重载时该怎么办呢?

1
2
3
4
5
6
7
// info.h
class A 中:
+ void a_signal();
+ void a_signal(int);
class B 中:
+ void b_slot();
+ void b_slot(int a);
1
2
3
4
5
6
7
// info.cpp
+ void B::b_slot() {
+ qDebug() << "start b_slot()";
+ }
+ void B::b_slot(int a) {
+ qDebug() << "start b_slot(int a)" << a;
+ }

此时回到 main.cpp ,就会发现已经报错了,编译一下:

error: no matching function for call to xxxxxxxx 就是,没有可以匹配的,不知道怎么匹配信号和槽

这时候,字符串形式的 connect 函数就可以解决该问题了,毕竟我们传信号函数和槽函数时,用的是 SIGNAL(a_signal())SLOT(b_slot()) 就直接代表,将两个无参数的函数连接起来

试试:

1
2
3
4
5
6
-   QObject::connect(&a, &A::a_signal, &b, &B::b_slot);
+ // QObject::connect(&a, &A::a_signal, &b, &B::b_slot);
+ QObject::connect(&a, SIGNAL(a_signal()), &b, SLOT(b_slot()));

getchar();
    emit a.a_signal(10);

运行,会发现没有输出,因为我们绑定的是无参数的信号函数,但是我们调用/发射的是有参数的信号。只要切换成调用/发射无参数的信号就可以运行并得到输出了

那么,我就不能继续使用函数指针的方式吗?不能破吗?

还是可以的— qOverload

上面说编译出错的地方是

1
QObject::connect(&a, &A::a_signal, &b, &B::b_slot);

只要我们通过 qOverload 写成:

1
2
3
4
5
6
// C++14
QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<>(&B::b_slot));
QObject::connect(&a, qOverload<int>(&A::a_signal), &b, qOverload<int>(&B::b_slot));
// C++11
QObject::connect(&a, QOverload<>::of(&A::a_signal), &b, QOverload<>::of(&B::b_slot));
QObject::connect(&a, QOverload<int>::of(&A::a_signal), &b, QOverload<int>::of(&B::b_slot));

写过之后其实已经就不会报错了,可以编译运行试试,也是不会报错的

参数匹配问题

两种情况:

  1. 信号传递参数,槽函数不需要参数,或者少于信号传递的参数
1
QObject::connect(&a, qOverload<int>(&A::a_signal), &b, qOverload<>(&B::b_slot));

此时是可以正常运行的

  1. 信号不传递参数,槽函数需要参数,或者信号传参少于槽函数需要的参数
1
QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<int>(&B::b_slot));

此时编译不能通过,会报错:error: static assertion failed: The slot requires more arguments than the signal provides.

意思就是槽函数需要的参数多余信号函数提供的参数

信号和槽的重复关联

假设我们连接两次:

1
2
3
4
QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<>(&B::b_slot));
QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<>(&B::b_slot));

emit a.a_signal();

输出:

1
2
3
start b_slot()
start b_slot()
=====================

也就是说,连接两次,发射信号的时候,就会执行两次槽函数

同理,一个信号连接两个槽函数也是一样的:

1
2
3
start b_slot()
start b_slot1()
=====================

其实一个信号连接一个槽函数两次,可以看作一个信号连接两个相同的槽函数一次

所以我们建立连接关系的时候,最好是放在一块来

connect 总结

总结一下,有 7 种情况

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
34
35
36
37
38
39
40
void test01()
{
A a;
B b;
// 1. 采用字符串方式进行信号和槽的链接关系
// 发出者指针,信号函数名称,接收者指针,槽函数名称
// QObject::connect(&a, SIGNAL(a_signal()), &b, SLOT(b_slot()));

// 2. 采用函数指针的方式来实现信号和槽的关系,编译期间会进行成员有效性的检测
// QObject::connect(&a, &A::a_signal, &b, &B::b_slot);

// 3. 信号和槽函数出现多个重载函数时,字符串支持性质更统一
// QObject::connect(&a, SIGNAL(a_signal()), &b, SLOT(b_slot()));
// QObject::connect(&a, SIGNAL(a_signal(int)), &b, SLOT(b_slot(int)));

// 4. 信号和槽函数出现多个重载函数时,继续使用函数指针的方法
// C++14
// QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<>(&B::b_slot));
// QObject::connect(&a, qOverload<int>(&A::a_signal), &b, qOverload<int>(&B::b_slot));
// C++11design
// QObject::connect(&a, QOverload<>::of(&A::a_signal), &b, QOverload<>::of(&B::b_slot));
// QObject::connect(&a, QOverload<int>::of(&A::a_signal), &b, QOverload<int>::of(&B::b_slot));

// 5. 信号传递参数,槽函数不需要参数,或者少于信号传递的参数
// QObject::connect(&a, qOverload<int>(&A::a_signal), &b, qOverload<>(&B::b_slot));
// 6. 信号不传递参数,槽函数需要参数,或者信号传参少于槽函数需要的参数
// QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<int>(&B::b_slot));

// 7. 信号和槽的重复关联
// 假设我们一个信号连接相同的槽函数两次
// QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<>(&B::b_slot));
// 假设我们一个信号连接两个不同的槽函数
// QObject::connect(&a, qOverload<>(&A::a_signal), &b, qOverload<>(&B::b_slot1));

getchar();
emit a.a_signal(); // 这里emit,点进去发现也是空,其实就是为了标识一下,知道是发信号的函数调用
}
/*

*/

槽函数的合并

拿 Windows 计算器举例:

这几个数字按键对应的槽函数极其相似,那我们能不能多个按键合并成一个槽函数呢?

但是问题又来了,合并后,怎么区分是哪个按键呢?—>就是接下来要说的内容了

1
2
3
4
5
6
7
// 获取发送者
[protected] QObject *QObject::sender() const
// Returns a pointer to the object that sent the signal, if called in a slot activated by a signal; otherwise it returns `nullptr`. The pointer is valid only during the execution of the slot that calls this function from this object's thread context.

// 强制转换
template <typename T> T qobject_cast(const QObject *object)
// Returns the given object cast to type T if the object is of type T (or of a subclass); otherwise returns nullptr. If object is nullptr then it will also return nullptr.

info.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
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <qtmetamacros.h>

#include <QObject>


class A : public QObject {
Q_OBJECT
public:
explicit A(int age = 10, QObject *parent = nullptr);
int getAge() const;
/* 自定义信号函数
* 1. 支持元对象系统
* 2. 访问权限,Qt 给了关键字 signals,用于定义信号函数
* 3. 信号本质就是一个成员函数,只写声明,不需要实现(MOC会自动生成代码)
* 4. 信号的返回值必须是 void ,参数随意
*/
signals:
void a_signal();
void a_signal(int);
void click();
private:
int m_age;
};

class B : public QObject {
Q_OBJECT
public:
explicit B(QObject *parent = nullptr);
/* 自定义槽函数
* 1. 支持元对象系统
* 2. 槽函数本质就是一个成员函数,访问权限 public/private/protected + slots
* 3. 槽函数必须有实现
* 4. 槽函数返回值和输入参数必须和信号一致
*/
public slots:
void b_slot();
void b_slot1();
void b_slot(int a);
void abc();
};

info.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
34
35
36
37
38
#include "info.h"

#include <QDebug>
#include <chrono>
#include <thread>


A::A(int age, QObject *parent) : QObject(parent), m_age(age) {}

int A::getAge() const {
return m_age;
}

B::B(QObject *parent) : QObject(parent) {}

void B::b_slot()
{
qDebug() << "start b_slot()";
}
void B::b_slot1()
{
qDebug() << "start b_slot1()";
}

void B::b_slot(int a)
{
qDebug() << "start b_slot(int a)" << a;
}

void B::abc() {
// 获取是哪个对象发送的信号导致这个槽函数被执行
A *obj = qobject_cast<A *>(sender());
if (obj == nullptr) {
qDebug() << "No Support!";
return ;
}
qDebug() << obj->getAge();
}

main.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
// 槽函数的合并
void test02() {
A x1(19);
A x2(22);
QWidget w;
B b;

QObject::connect(&x1, &A::click, &b, &B::abc);
QObject::connect(&x2, &A::click, &b, &B::abc);
QObject::connect(&w, &QWidget::destroyed, &b, &B::abc);

// 测试几个调用槽函数的方式
// emit x1.click(); // 信号1调用槽函数
// emit x2.click(); // 信号2调用槽函数
// b.abc(); // 直接调用槽函数
// w.destroyed(); // 非A类对象用信号调用槽函数
}

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
test02();
qDebug() << "=====================";
return app.exec();
return 0;
}