信号和槽机制
OVERVIEW
- 信号和槽机制
- 一、系统自带信号与槽
- 二、自定义信号与槽
- 1.基本使用
- student.cpp
- teacher.cpp
- widget.cpp
- main.cpp
- 2.信号与槽重载
- student.cpp
- teacher.cpp
- widget.cpp
- main.cpp
- 3.信号连接信号
- 4.Lambda表达式
- 5.信号与槽总结
信号槽机制是 Qt 框架引以为豪的机制之一。
所谓信号槽实际就是观察者模式,将要处理的信号和自己的一个函数/槽slot绑定来处理信号,当某个事件发生之后检测对象就会发出信号(无目的的广播),如果有其他对象对这个信号感兴趣就会使用connect函数进行连接。
即当信号发出时,被连接的槽函数会自动被回调。类似观察者模式:当发生了感兴趣的事件,某个操作就会被自动触发。
一、系统自带信号与槽
信号槽的优点:松散耦合,信号发送方和接收方本身是没有关联的,通过connect连接将两端耦合在一起
QPushButton * quitBtn = new QPushButton("quit",this);//创建关闭按钮
connect(quitBtn, &QPushButton::clicked, this, &QWidget::close);//信号槽使用方式
函数connect(sender, signal, receiver, slot);
参数解释:
- sender:发出信号的对象
- signal:发送对象发出的信号
- receiver:接收信号的对象
- slot:接收对象在接收到信号之后所需要调用的函数(槽函数)
利用帮助文档了查找系统自带的信号和槽,在帮助文档中如按钮的点击信号输入QPushButton,首先我们可以在Contents中寻找关键字 signals信号的意思,但是我们发现并没有找到,这时候我们应该想到也许这个信号的被父类继承下来的,因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个。
这里的clicked就是我们要找到,槽函数的寻找方式和信号一样,只不过他的关键字是slot。
二、自定义信号与槽
使用connect可使用系统提供的信号和槽,
但Qt的信号槽机制并不仅仅是使用系统提供那部分,其还允许开发者设计自己的信号和槽,
1.基本使用
student.cpp
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject {
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
signals:
public slots:
//槽函数slot 返回值是void 需要声明需要实现 可以有参数(发生重载)
void treat();
};
#endif // STUDENT_H
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent) { }
void Student::treat() {
qDebug() << "debug: receive signal. student treats teacher.";
}
teacher.cpp
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject {
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
//信号signal 返回值是void 只需要声明不需要实现 可以有参数(发生重载)
void hungry();
public slots:
};
#endif // TEACHER_H
#include "teacher.h"
Teacher::Teacher(QObject *parent) : QObject(parent) { }
widget.cpp
#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();
void classIsOver();
private:
Ui::Widget *ui;
Teacher *tch;
Student *stu;
};
#endif // WIDGET_H
#include <QPushButton>
#include <QDebug>
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
//1.第一次绑定 使用系统自带信号与槽函数
QPushButton *mybtn = new QPushButton("myBtn", this);
mybtn->resize(100, 30);
connect(mybtn, &QPushButton::clicked, this, &Widget::classIsOver);//按钮触发classIsOver函数
//2.第二次绑定 使用自定义信号与槽函数
tch = new Teacher(this);//初始化老师对象
stu = new Student(this);//初始化学生对象
connect(tch, &Teacher::hungry, stu, &Student::treat);//老师饿了时学生主动请客的connect连接
}
Widget::~Widget() {
delete ui;
}
void Widget::classIsOver() {
qDebug() << "debug: btn is pushed and Widget::classIsOver was called. emitting signal...";
emit tch->hungry();//函数被调用 则触发发送老师饿了的信号
}
main.cpp
#include <QApplication>
#include "widget.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
2.信号与槽重载
student.cpp
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject {
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
signals:
public slots:
//槽函数slot 返回值是void 需要声明需要实现 可以有参数(发生重载)
void treat();
void treat(QString food);
};
#endif // STUDENT_H
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent) { }
void Student::treat() {
qDebug() << "debug: receive signal. student treats teacher.";
}
void Student::treat(QString food) {
qDebug() << "debug: receive signal. student treats teacher with " << food.toUtf8().data();
}
teacher.cpp
#ifndef TEACHER_H
#define TEACHER_H
#include <QObject>
class Teacher : public QObject {
Q_OBJECT
public:
explicit Teacher(QObject *parent = nullptr);
signals:
//信号signal 返回值是void 只需要声明不需要实现 可以有参数(发生重载)
void hungry();
void hungry(QString food);
public slots:
};
#endif // TEACHER_H
#include "teacher.h"
Teacher::Teacher(QObject *parent) : QObject(parent) { }
widget.cpp
#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();
void classIsOver1();
void classIsOver2();
private:
Ui::Widget *ui;
Teacher *tch;
Student *stu;
};
#endif // WIDGET_H
#include <QPushButton>
#include <QDebug>
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
//1.第一次绑定 使用系统自带信号与槽函数
QPushButton *mybtn1 = new QPushButton("myBtn1", this);
mybtn1->resize(100, 30);
connect(mybtn1, &QPushButton::clicked, this, &Widget::classIsOver1);//按钮触发classIsOver函数
QPushButton *mybtn2 = new QPushButton("myBtn2", this);
mybtn2->move(100, 0);
mybtn2->resize(100, 30);
connect(mybtn2, &QPushButton::clicked, this, &Widget::classIsOver2);//按钮触发classIsOver函数
tch = new Teacher(this);//初始化老师对象
stu = new Student(this);//初始化学生对象
//2.第二次绑定 使用自定义信号与槽函数
void(Teacher::*tchsignal_)() = &Teacher::hungry;
void(Student::*stusignal_)() = &Student::treat;
connect(tch, tchsignal_, stu, stusignal_);//老师饿了时学生主动请客的connect连接
//3.第三次绑定 使用自定义信号与槽函数
//定义函数指针 用于识别重载的函数地址
void(Teacher::*tchsignal)(QString) = &Teacher::hungry;
void(Student::*stusignal)(QString) = &Student::treat;
connect(tch, tchsignal, stu, stusignal);
}
Widget::~Widget() {
delete ui;
}
void Widget::classIsOver1() {
qDebug() << "debug: btn1 is pushed and Widget::classIsOver1 was called. emitting signal...";
emit tch->hungry();//函数被调用 则触发发送老师饿了的信号
}
void Widget::classIsOver2() {
qDebug() << "debug: btn2 is pushed and Widget::classIsOver2 was called. emitting signal...";
emit tch->hungry("apple pie");
}
main.cpp
#include <QApplication>
#include "widget.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
3.信号连接信号
之前的使用中都是利用槽函数实现的,点击btn按钮触发函数调用,函数调用后emit发出信号再触发slot槽函数,从而实现连续的效果。
也可以使用另一种方式信号触发信号完成,利用点击btn按钮的信号去触发另一个信号,
主要对widget.cpp中的内容进行修改,修改后如下:
#include <QPushButton>
#include <QDebug>
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
//1.第一次绑定 使用系统自带信号与槽函数
QPushButton *mybtn1 = new QPushButton("myBtn1", this);
mybtn1->resize(100, 30);
//connect(mybtn1, &QPushButton::clicked, this, &Widget::classIsOver1);//按钮触发classIsOver函数
QPushButton *mybtn2 = new QPushButton("myBtn2", this);
mybtn2->move(100, 0);
mybtn2->resize(100, 30);
//connect(mybtn2, &QPushButton::clicked, this, &Widget::classIsOver2);//按钮触发classIsOver函数
tch = new Teacher(this);//初始化老师对象
stu = new Student(this);//初始化学生对象
//2.第二次绑定 使用自定义信号与槽函数
void(Teacher::*tchsignal_)() = &Teacher::hungry;
void(Student::*stusignal_)() = &Student::treat;
connect(tch, tchsignal_, stu, stusignal_);//老师饿了时学生主动请客的connect连接
connect(mybtn1, &QPushButton::clicked, tch, tchsignal_);//信号连接信号
//3.第三次绑定 使用自定义信号与槽函数
//定义函数指针 用于识别重载的函数地址
void(Teacher::*tchsignal)(QString) = &Teacher::hungry;
void(Student::*stusignal)(QString) = &Student::treat;
connect(tch, tchsignal, stu, stusignal);
//connect(mybtn2, &QPushButton::clicked, tch, tchsignal);//信号连接信号
connect(mybtn2, &QPushButton::clicked, this, [=](){
emit tch->hungry("apple pie");
});
}
Widget::~Widget() {
delete ui;
}
void Widget::classIsOver1() {
qDebug() << "debug: btn1 is pushed and Widget::classIsOver1 was called. emitting signal...";
emit tch->hungry();//函数被调用 则触发发送老师饿了的信号
}
void Widget::classIsOver2() {
qDebug() << "debug: btn2 is pushed and Widget::classIsOver2 was called. emitting signal...";
emit tch->hungry("apple pie");
}
btn触发信号连接信号,跳过了classIsOver函数的中间调用过程,直接实现了最终结果的输出。
4.Lambda表达式
C++11中的Lambda表达式用于定义并创建匿名的函数对象以简化编程工作,Lambda表达式的基本构成:
[capture](parameters)mutable->returnType {
statement
}
-
函数对象参数;
[]
,标识一个Lambda的开始,这部分必须存在不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:- void,没有使用任何函数对象参数。
- =,函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)
- &,函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)
- this,函数体内可以使用Lambda所在类中的成员变量
- 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修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)QPushButton * myBtn = new QPushButton (this); QPushButton * myBtn2 = new QPushButton (this); myBtn2->move(100,100); int m = 10; connect(myBtn,&QPushButton::clicked,this,[m] ()mutable { m = 100 + 10; qDebug() << m; }); connect(myBtn2,&QPushButton::clicked,this,[=] () { qDebug() << m; }); qDebug() << m;
-
函数返回值;
->
返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。 -
是函数体;
{}
,标识函数的实现这部分不能省略,但函数体可以为空。
利用Lambda表达式简化信号与槽的使用机制:利用lambda表达式实现点击按钮,关闭窗口:
QPushButton *btn = new QPushButton;
btn->setText("quit");
connect(btn, &QPushButton::clicked, this, [=](){
this->close();
});
5.信号与槽总结
自定义信号槽需要注意的事项:
- 发送者和接收者都需要是QObject的子类(槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
- 自定义信号signal返回值是 void,只需要声明不需要实现,可以有参数(发生重载)
- 槽函数是普通的成员函数,需要声明也需要实现,会受到 public、private、protected 的影响(早期必须写到public slots作用域下)
- 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数
信号与槽的连接:connect函数
- 一个信号可以和多个槽函数相连:如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的
- 多个信号可以连接到一个槽:只要任意一个信号发出,这个槽就会被调用
- 一个信号可以连接到另外的一个信号:当第一个信号发出时第二个信号被发出,除此之外这种信号-信号的形式和信号-槽的形式没有什么区别。
- 信号槽要求信号和槽的参数类型对应(参数的个数信号可以多于槽函数,反之报错)
- 若信号和槽的参数不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少)。
- 槽可以被取消链接:这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽
- 使用Lambda 表达式:在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的,在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。
QT4版本的信号槽写法:Qt5在语法上完全兼容Qt4。
connect(tch, SIGNAL(hungry(QString)), stu, SLOT(treat(QString)));
这里使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意到connect函数的signal和slot都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。