Qt信号与槽机制
信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
使用系统自带的信号和槽实现关闭窗口功能
首先需要创建一个按钮用于触发“关闭窗口”信号:
1
|
QPushButton *btn = new QPushButton("关闭窗口", this);
|
然后我们可以通过connect函数进行信号的连接操作,connect函数的一般形式为:
1
|
connect(sender, signal, receiver, slot);
|
参数解释:
- sender:发出信号的对象
- signal:发送对象发出的信号(函数的地址)
- receiver:接收信号的对象
- slot:接收对象在接收到信号之后所需要调用的函数(槽函数)
此处即体现出信号和槽机制松散耦合的优点:信号发送端和接收端本身没有关联,但可以通过connect进行连接将两端耦合在一起。 随后,填入相应实参即可调用connect函数进行信号连接。
1
|
connect(btn, &QPushButton::clicked, this, &Widget::close);
|
那么系统自带的信号和槽通常如何查找呢?这个就需要利用帮助文档了,比如这里我们需要的是QPushButton的点击信号,在帮助文档中输入QPushButton,首先我们可以在Contents中寻找关键字 signals,但发现并没有找到,这时候我们应该想到也许这个信号的被父类继承下来的,因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个
1
2
3
4
5
6
|
void clicked(bool checked = false)
void pressed()
void released()
void toggled(bool checked)
- 3 signals inherited from QWidget
- 2 signals inherited from QObiect
|
这里的clicked就是我们需要的signal函数,槽函数的寻找方式和信号一样,只不过他的关键字是slot。
自定义信号与槽函数
定义两个继承自QObject的类——Teacher和Student,实现一个场景:上课铃响后,老师喊上课,学生们收到上课的信号回教室坐好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject
{
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
// 自定义信号 写到signal下
// 返回值是void,只需要声明,不需要实现
// 可以有参数,可以发生重载
void ClassBegin();
public slots:
// 自定义槽函数,写到slot下
};
#endif // TEACHER_H
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
signals:
public slots:
// 早期Qt版本必须要写到public slots下,高级版本可以写到public或者全局下
// 返回值void,需要声明也需要实现
// 可以有参数,也可以发生重载
void SitDown();
};
#endif // STUDENT_H
|
1
2
3
4
5
6
7
8
9
10
|
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent)
{
}
// 自定义成员函数记得加函数返回值类型
void Student::SitDown(){
qDebug() << "学生都坐好" << endl;
}
|
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
|
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "teacher.h"
#include "student.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 声明teacher和student类型成员变量
Teacher *tp;
Student *sp;
// 声明上课铃响函数
void Ring();
};
#endif // 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
28
29
30
31
32
33
34
35
36
37
|
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton *btn = new QPushButton("关闭窗口", this);
/*
* 新建信号连接函数
* 参数1:信号发送者
* 参数2:发送的具体的信号(函数的地址)
* 参数3:信号的接收者
* 参数4:信号的处理——槽函数地址(槽)
* 信号和槽的优点——松散耦合:信号发送端和接收端本身没有关联,但可以通过connect进行连接将两端耦合在一起
*/
// 参数2和参数4写父类子类都可以
connect(btn, &QPushButton::clicked, this, &Widget::close);
// 创建对象并指定父窗口
this->tp = new Teacher(this);
this->sp = new Student(this);
// 情景:上课铃响后,老师发出上课信号,学生响应信号,坐好
// 老师说上课,学生坐好的连接
connect(tp, &Teacher::ClassBegin, sp, &Student::SitDown);
// 调用上课铃响函数
Ring();
}
// 自定义的类方法记得加返回值类型
void Widget::Ring(){
// 上课函数,调用后触发老师喊上课的信号
emit tp->ClassBegin();
}
Widget::~Widget()
{
delete ui;
}
|
此时的代码执行流程为:
调用Ring()函数–>触发老师喊上课信号–>回调connect函数–>学生收到信号–>调用SitDown函数
重载信号与槽函数
信号和槽函数均可发生重载
1
|
void SitDown(QString ClassName);
|
1
2
3
4
5
6
7
8
|
void Student::SitDown(QString ClassName){
// 直接使用此方式打印会使得ClassName带引号(QString类型字符串),需要转换为char*类型字符串
// qDebug() << "学生都坐好并拿出" << ClassName << "课本" << endl;
// QString -> char* 先转换成QByteArray再转成char*
// 下面这种方式会多出一个空格
// qDebug() << "学生都坐好并拿出" << ClassName.toUtf8().data() << "课本" << endl;
qDebug() << ("学生都坐好并拿出" + ClassName + "课本").toUtf8().data() << endl;
}
|
1
|
void ClassBegin(QString ClassName);
|
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
|
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
...
// 创建对象并指定父窗口
this->tp = new Teacher(this);
this->sp = new Student(this);
// 情景:上课铃响后,老师发出上课信号,学生响应信号,坐好
// 老师说上课,学生坐好的连接
// 当发生信号或槽函数重载后,connect无法分清是哪个函数发生了重载,需要通过函数指针重新指定函数地址
// connect(tp, &Teacher::ClassBegin, sp, &Student::SitDown);
// 函数指针写法:
// 函数返回值类型(命名空间:: *指针名)(参数类型1, 参数类型2...) = 函数地址
void(Teacher:: *teachersignal)(QString) = &Teacher::ClassBegin;
void(Student:: *studentslot)(QString) = &Student::SitDown;
connect(tp, teachersignal, sp, studentslot);
// 调用上课铃响函数
Ring();
}
// 自定义的类方法记得加返回值类型
void Widget::Ring(){
// 上课函数,调用后触发老师喊上课的信号
// emit tp->ClassBegin();
emit tp->ClassBegin("语文");
}
|
使用信号连接信号
程序情景:点击一个上课的按钮,再触发上课铃响,然后老师叫同学们坐好上课。
1
2
3
4
5
6
7
8
9
10
11
|
// 通过函数指针重新指定有参函数的地址
void(Teacher:: *teachersignal)(QString) = &Teacher::ClassBegin;
void(Student:: *studentslot)(QString) = &Student::SitDown;
// 通过函数指针重新指定无参函数的地址
void(Teacher:: *tsignal)(void) = &Teacher::ClassBegin;
void(Student:: *ssignal)(void) = &Student::SitDown;
connect(tp, teachersignal, sp, studentslot);
// 使用按钮调用Ring函数
QPushButton *classbtn = new QPushButton("上课", this);
classbtn->move(100, 0);
connect(classbtn, &QPushButton::clicked, this, &Widget::Ring);
|
程序情景:点击上课的按钮直接触发老师喊上课信号,然后老师喊同学们坐好上课。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void(Teacher:: *teachersignal)(QString) = &Teacher::ClassBegin;
void(Student:: *studentslot)(QString) = &Student::SitDown;
// 通过函数指针重新指定无参函数的地址
void(Teacher:: *tsignal)(void) = &Teacher::ClassBegin;
void(Student:: *ssignal)(void) = &Student::SitDown;
connect(tp, teachersignal, sp, studentslot);
// 使用按钮调用Ring函数
QPushButton *classbtn = new QPushButton("上课", this);
classbtn->move(100, 0);
// connect(classbtn, &QPushButton::clicked, this, &Widget::Ring);
// 使用按钮直接连接老师上课信号(有参),让老师上课信号作为槽函数再连接学生坐好的槽函数
connect(classbtn, &QPushButton::clicked, tp, tsignal);
connect(tp, tsignal, sp, ssignal);
|
程序情景:点击上课按钮触发老师喊上课的信号,但学生不听老师的,不坐好
1
2
|
// 使用disconnect函数断开连接
disconnect(tp, tsignal, sp, ssignal);
|
Qt4版本信号和槽的写法
1
|
connect(zt,SIGNAL(ClassBegin(QString)),st,SLOT(SitDown(QString)));
|
这里使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意到**connect()**函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。
Qt5在语法上完全兼容Qt4,而反之是不可以的。
Lambda表达式
Lambda是属于C++11的新特性。在早期版本(Qt4之前)需要在.pro文件中添加:CONFIG += c++11
C++中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。Lambda表达式的基本构成为:
1
2
3
4
|
// 函数定义:
[函数对象参数](操作符重载函数参数)mutable->返回值类型{函数体}
// 函数调用:
();
|
函数对象参数[],标识一个Lambda的开始,必须存在。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:
函数对象参数 |
解析 |
空 |
没有使用任何函数对象参数。 |
= |
函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。 |
this |
函数体内可以使用Lambda所在类中的成员变量。若在connect函数中使用this时,前面的信号接收者(或信号发送者)可省略 |
a |
将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。 |
&a |
将a按引用进行传递。 |
a, &b |
将a按值进行传递,b按引用进行传递。 |
=, &a, &b |
除a和b按引用进行传递外,其他参数都按值进行传递。 |
&, a, b |
除a和b按值进行传递外,其他参数都按引用进行传递。 |
操作符重载函数参数标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
可修改标示符即mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身,再次访问传入的函数对象参数时仍是原值)。
1
2
3
4
5
6
7
8
9
10
|
QPushButton * myBtn = new QPushButton (this);
QPushButton * myBtn2 = new QPushButton (this);
myBtn2->move(100,100);
int m = 10;
// 点击按钮输出110
connect(myBtn,&QPushButton::clicked,this,[m] ()mutable { m = 100 + 10; qDebug() << m; });
// 点击按钮输出10
connect(myBtn2,&QPushButton::clicked,this,[=] () { qDebug() << m; });
// 输出10
qDebug() << m;
|
函数返回值->返回值类型,标识函数返回值的类型。当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
函数体{}标识函数的实现,这部分不能省略,但函数体可以为空。
使用Lambda表达式的优点:
在connect函数中使用Lambda表达式,可以更加方便地使用有参槽函数。
信号和槽总结
- 信号可以连接信号
- 一个信号可以连接多个槽函数。如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
- 多个信号也可以连接同一个槽函数。只要任意一个信号发出,这个槽就会被调用。
- 信号和槽函数的参数类型必须一一对应
- 信号的参数个数可以多于槽函数的参数个数,但除了多出来的参数,剩下的参数类型仍然要一一对应。
- 信号槽可以通过disconnect函数断开连接
- 槽可以被取消链接。但这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。
- 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
- 信号和槽函数返回值是 void
- 信号只需要声明,不需要实现
- 槽函数需要声明也需要实现
- 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
- 使用 emit 在恰当的位置发送信号;
- 使用connect()函数连接信号和槽。
- 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数