目录
一、信号和槽概述
“信号”这个词在我们的日常生活中随处可见,比如:信号灯变绿,我们行人进行通行、鸡叫表示天亮了、下课铃响了代表着下课了、王者连跪表示我们该充钱了等等信号这个概念,在我们的生活中随处可见。从以上的例子中我们仔细观察的话其实是会发现,每一个信号后面都对应着我们的一个动作,比如:绿灯了,我们要通过人行道、鸡叫了我们要起床了、下课了我们要去干饭、王者连跪了我们要打开微信充钱…这些信号后面伴随的动作是怎么来到?或者说我们怎么知道接收到这些信号后该做什么?当然是生活经验、和老师学校教的反正不可能是临时起意嘛!换而言之就是这些信号后面代表的动作都是我们早已知道的!
在Linux系统中,我们也介绍了信号的产生、信号的检测以及信号的处理机制,它就是系统内部的通知机制,也可以是一种进程间通信的方式。在系统中有很多信号,我们可以通过signal()函数捕捉信号,重写一个信号处理函数。在Qt中的信号也和系统中的信号有相似之处。
在QT中也是同理,信号就代表着用户发出了一个指令,而槽就代表着对于这个指令的处理方法!
- 在Qt中,用户和控件的每次交互过程称为一个事件。比如"用户点击按钮"是一个事件,"用户关闭窗口"也是一个事件。每个事件都会发出一个信号,例如用户点击按钮会发出"按钮被点击"的信号,用户关闭窗口会发出"窗口被关闭"的信号。
- Qt中的所有控件都具有接收信号的能力,一个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作。例如,按钮所在的窗口接收到"按钮被点击"的信号后,会做出"关闭自己"的响应动作;再比如输入框自己接收到"输入框被点击"的信号后,会做出"显示闪烁的光标,等待用户输入数据"的响应动作。在Qt中,对信号做出的响应动作就称之为槽。
- 信号和槽是Qt特有的消息传输机制,它能将相互独立的控件关联起来。比如,"按钮"和"窗口"本身是两个独立的控件,点击"按钮"并不会对"窗口"造成任何影响。通过信号和槽机制,可以将"按钮"和"窗口"关联起来,实现"点击按钮会使窗口关闭"的效果。
信号的本质
信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时Qt对应的窗口类会发出某个信号,以此对用户的操作做出反应。因此,信号的本质就是事件。如:
- 按钮单击、双击
- 窗口刷新
- 鼠标移动、鼠标按下、鼠标释放,
- 键盘输入
在 Qt 中信号是通过什么形式呈现给使用者呢?
- 我们对哪个窗口进行操作,哪个窗口就可以捕捉到这些被触发的事件。
- 对于使用者来说触发了一个事件我们就可以得到Qt框架给我们发出的某个特定信号。
- 信号的呈现形式就是函数,也就是说某个事件产生了,Qt 框架就会调用某个对应的信号函数,通知使用者。
槽的本质
槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何位置( public、protected 或private), 可以具有任何参数,可以被重载,也可以被直接调用(但是不能有默认参数)。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
说明
(1) 信号和槽机制底层是通过函数间的相互调用实现的。每个信号都可以用函数来表示,称为信号函数;每个槽也可以用函数表示,称为槽函数。 例如: "按钮被按下"这个信号可以用clicked()函数表示,"窗口关闭"这个槽可以用close()函数表示,假如使用信号和槽机制实现: "点击按钮会关闭窗口"的功能,其实就是clicked()函数调用close()函数的效果。
(2) 信号函数和槽函数通常位于某个类中,和普通的成员函数相比,它们的特别之处在于:
- 信号函数用 signals 关键字修饰,槽函数用public slots、protected slots或者private slots修饰。signals 和 slots 是Qt在C++的基础上扩展的关键字,专门用来指明信号函数和槽函数;
- 信号函数只需要声明,不需要定义(实现) , 而槽函数需要定义(实现)。
信号函数的定义是Qt自动在编译程序之前生成的.编写Qt应用程序的程序员无需关注。这种自动生成代码的机制称为元编程(Meta Programming) .这种操作在很多场景中都能见到。
Qt中的信号也要涉及信号三要素:信号源、信号类型和信号处理方式:
- 信号源:Qt中的信号是由某个控件发出的,Linux系统中可以是一个进程发送的信号。
- 信号类型:用户不同的操作会触发不同的信号,例如点击按钮就会触发点击信号(clicked)、在输入框中移动光标也会触发相应的信号,我们写这样的GUI程序就是为了和用户交互,所以必须知道当前用户的具体操作,通过不同的操作进行不同的处理。
- 信号处理方式:在Qt中就引入了一个概念就是槽(slot),说白了就是一个函数,再使用connect这样的函数把一个信号和一个槽关联起来,之后只要触发了信号,Qt就会自动执行槽函数,这种槽函数本质上也是一种回调函数。
所以在Qt中一定要先关联信号和槽,也就是先要有槽函数并使用connect函数将二者关联起来,然后再触发这个信号,顺序不能颠倒。
二、信号和槽的使用
(一)connect函数
上面既然说了,信号的三要素是:发送源、信号类型、处理方法,可是对于QT来说,它怎么知道哪一个信号该调用哪一个处理方法呢?也就是说,对于QT来说,它怎么知道某个信号该调用哪个处理方法呢?
当然,是我们开发人员提前将信号与处理方法之间的关系绑定好,到时候QT直接进行调用即可!
在 Qt 中,QObject类 中提供了一个静态成员函数 connect() ,该函数专门用来关联指定的信号函数和槽函数。
关于QObject类:
QObject类实际上是QT中所有内置类型的祖宗类!也就是说在QT中大部分类要么直接继承自QObject类,要么间接继承自QObject类,比如 QWidget 就继承自 QObject。与Java中的继承十分相似!
在QT中大概的继承关系如下(不准确的继承):
以下是 connect 的函数原型:
connect (const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection)
sender: 哪个控件发出的信号
signal: 发出的什么信号(信号函数)
receiver: 谁接收这个信号
method: 怎么处理这个信号(接收信号的槽函数)
type: 用户指定关联方式,常用默认值,一般不需要手动设置.
connect 要求第一个参数和第二个参数是匹配的,比如第一个参数的类型如果是 QPushButton* ,那么第二个参数的信号必须是 QPushButton 内置的信号(父类的信号),不能是其他的一个类。
(二)实现一个点击按钮关闭窗口的功能
我们在窗口上设置一个按钮,当我们按下这个按钮就可关闭窗口。
分析: 当用户按下按钮控件过后,实际上是向QT发送了一个叫clicked的信号,为此我们现在要做的就是捕捉该信号,为该信号关联一个关闭函数,也就是对应Widget类中的 close() 函数。
具体代码如下:
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 点击按钮,窗口关闭
QPushButton *button = new QPushButton(this); // 创建一个button对象
button->setText("关闭"); // 设置按钮上的文本
button->move(200, 200); // 设置按钮的位置
connect(button, &QPushButton::clicked, this, &Widget::close);
}
Widget::~Widget()
{
delete ui;
}
- 创建一个QPushButton对象,想要点击这个按钮后就关闭这个窗口,发出信号的一定是QPushButton对象。
- 发出的信号一定是QPushButton内置的clicked信号。
- 要交给的对象就是this也就是Widget对象。
- 处理函数就可以使用Widget继承的QWidget内置的close()槽函数。
注意这里是 clicked,而不是 click:
运行程序后,点击按钮成功关闭窗口:
(三)再谈connect
但是对以上的代码我们有两个疑问?
我们怎么知道点击一下按钮就会发出一个clicked信号?或者我们怎么知道QT有哪些信号或槽函数?
答:多看官方文档;当我们不了解某一个知识点的时候,官方文档是最好的学习工具,况且QT自己在也已经内置了官方文档:
如果我们要查找某一个类的信号的话,我们可以直接对这个类进行搜索,当然可能这个类中并没有包含整个信号,那么我们可以对这个类的父类、祖宗类进行搜索,这样的话我们一定可以找到这个信号,就比如上图的clicked信号,在QPushButton中就没有,但是在其父类QAbstractButton就存在:
找了一遍后发现没有找到clicked这样的信号,既然这样就要向它的父类中查找。
QPushButton的父类就是QAbstractButton,通过类名我们也可以得知这是一个抽象类,在这个页面向下查找就找到了Signals这块,这里就有clicked等信号,也就类似于函数。
对于槽函数我们也是如此:
在查看官方文档的时候,connect的原型如下 :
第一个和第三个参数我们传入的是 QObject 的子类指针,所以没有问题,但是第二个和第四个参数传入的是函数指针,但是为什么使用 const char* 来接收呢?( 并且这在C++中是绝对禁止的!) 就拿上面connect的例子举例,clicked的类型应该是void ( * ) () ,而close的类型应该是 bool ( * ) (),可是为什么这两个函数指针类型可以直接赋值给 const char*? 这两个的意义可不一样。
- 原因就是这是旧版本的 connect 函数声明,是在对于信号传参的时候需要用SIGNAL宏修饰一下,对于槽函数传参的时候需要使用SLOT宏修饰一下,通过宏替换就可以把函数指针转换成 char*,这样就不会有类型不匹配的问题了。
- 官方文档里的这个函数声明是旧版本的 connect 声明 ,正确写法是:
- connect(button1, SIGNAL(&QPushButton::clicked), this, SLOT(&Widget::close))
从 Qt5 开始,对该写法进行了简化,不再需要写这两个宏了,所以上面的代码才能直接传入函数指针。Qt5 开始,给 connect 提供了重载版本,第二个参数和第四个参数成了泛型参数,允许传入任意类型的函数指针了。
点击 ctrl 然后鼠标点击 connect ,跳转到源码中查看:
这样的话,对于一个函数指针来说,它就不用再使用SIGNAL宏和SLOT宏来进行修饰了,根据模板的特性,它会自动推导传递过来的函数指针的类型,极其方便!同时新版的connect还使用了“类型萃取”技术,它可以帮助我们检查参数,发送源控件与信号类型是否匹配:若传入的第一个参数和第二个参数,或者第三个参数和第四个参数不匹配,(不匹配是指:2、4参数传入的函数指针,不是1、3参数的成员函数的指针。)代码出现编译错误。
三、自定义槽函数
纯代码
虽然QT已经给我们内置了许多对应的槽函数,但是在我们实际的开发中,我们更多的情况下,是结合自己的业务场景,针对对应的信号量身定做一个槽函数,也就是自定义槽函数。
自定义参函数的语法:
- 所谓的 slot 就是一个普通的成员函数,所以所谓的自定义一个槽函数,操作过程和自定义一个普通的成员函数没啥区别。
- 该成员函数需要使用public signals、private signals、protected signals来进行修饰。(在QT 5及以上版本中可以不用signal修饰)
为此,为了演示自定义槽函数,这也是上一篇也用过的,先使用纯代码的方式实现一下自定义槽。我们设计出一个具体的场景:
当用户点击按钮的时候就可以切换窗口标题。
分析: 当用户点击按钮的时候,实际上会发出一个clicked信号,现在对应的处理动作是将窗口标题改为:“捕捉到按钮信号”,为此我们需要捕捉clicked信号,并且自定义处理函数。
- 还是要先new一个QPushButton对象,把点击信号和槽函数关联起来。
- 槽函数中就设置为,捕捉到了信号就把Widget界面的标题修改一下。
具体代码如下:
widget.h:记得声明函数
#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 handleClicked();
void handleClicked(); // 这两者自定义槽函数的方式都是可以的
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton *button = new QPushButton(this); // 创建一个button对象
button->setText("按钮"); // 设置按钮上的文本
button->move(200, 200); // 设置按钮的位置
connect(button, &QPushButton::clicked, this, &Widget::handleClicked);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleClicked()
{
// 按下按钮,修改窗口标题
this->setWindowTitle("捕捉到按钮信号");
}
运行程序,查看效果:
窗口标题成功修改:
图形化界面
在 ui 界面,拖动 pushbutton 到编辑界面然后输入文字后,右键单击该按钮,选择 “转到槽”
这个窗口为我们提供了 QPushButton 的所有信号,还包括了 QPushButton的父类信号。选择第一个信号:
点击确定或者双击就会跳转到编辑widget.cpp的界面,此时函数的声明和定义都已经自动生成好了,编写代码即可:
【注意】:这种方式是不会出现 connect() 函数的,Qt中除了connect可以连接信号和槽外,还可以通过函数名字的方式来自动连接,如上图所示函数名格式:on_ XXX_ SSS,其中:
- 以"on"开头,中间使用下划线连接起来;
- " XXX "表示的是对象名(控件的objectName)。
- " SSS "表示的是对应的信号。
- 如: " on_ pushButton_ _clicked() " , pushButton代表的是对象名,clicked 是对应的信号。
点击按钮,改变窗口的标题:
这里名字故意写错,就不能达到我们想要的效果:
这都是Qt中调用connectSlotsByName这个函数触发自动连接信号和槽,这个函数是在自动生成的ui_widget.h中setupUi函数中调用的。
所以,如果是通过图形化界面创建控件,还是使用第二种方式快速连接信号和槽;如果是通过代码的方式创建控件,还是得手动调用connect函数,原因就是没有调用connectSlotsByName函数。
四、自定义信号
自定义信号很少用到。因为在GUI中,用户的操作行为是可以穷举的,Qt内置的信号已经覆盖到了大部分可能的用户操作,对于某些场景还是需要我们使用自定义信号的!
- 信号是一种特殊的函数,程序员只需写出函数声明,并告诉Qt(需要使用slots关键字进行修饰),这是一个信号即可。这个函数的定义,是Qt在编译过程中,自动生成的(无法干预)。
- 信号函数的返回值必须是void,有没有参数都可以,也支持函数重载。
- 信号可以使用emit关键字进行发射(Qt5 emit不写也行)。
为此,我们根据该场景设计出一个具体的业务场景:
当用户点击按钮就可以切换窗口标题
signals是Qt自己扩展的关键字,qmake会调用代码分析和生成的工具,识别到signals这个关键字时就会把下面的函数声明认为是信号,并自动生成函数定义:
通过connect函数连接信号和槽函数就可以了,但是光连接还是不够的,还需要发送我们自定义的信号,这个emit也是Qt中的关键字,作用就是发送信号:
当点击按钮,就会触发on_pushButton_clicked函数,函数中就会发送mySignal信号,收到信号就会执行handleSignal自定义函数,之后就会修改窗口标题:
五、带参数的信号和槽
在自定义信号和槽函数的时候可以带上参数,并且QT允许重载信号和槽函数。信号在和槽函数进行绑定的时候,主要表现在以下两点:
- 槽函数的参数类型必须和它绑定的信号的参数类型一致。
- 槽函数的参数个数可以少于等于它绑定的信号的参数的个数。
参数的类型必须要一致,个数不一致是可以的,但是要求信号的参数的个数必须要比槽的参数多。
通过这一次连接信号和槽,并搭配不同参数就可以实现不同结果,可以让代码复用,就比如:
点击按按钮1,左上角标题改为标题1,点击按钮2,左上角标题改为标题2:
而且信号的参数比槽函数多也是可以的,但是不允许槽函数中的参数比信号中的多,原因就是:
- 一个槽函数可能绑定多个信号。
- 如果严格要求就意味着信号绑定到槽的要求变高了。
- 所以信号的参数个数可以大于槽的参数个数,这样让信号和槽之间的绑定变得更灵活,更多的信号可以绑定到这个槽函数上。
- 虽然个数不一致,但是槽函数会按照参数顺序拿前N个参数,这样就可以确保槽函数的每个参数都有值。
还有一点就是,如果想要使用信号和槽的机制,就必须在类的一开始写上Q_OBJECT宏,否则会出现编译错误。
当我们转到定义就可以看到这个宏中展开的代码,这里也有很多的宏,也会继续展开,最终实现相应的功能:
六、信号和槽存在的意义
信号和槽最主要就是要解决响应用户的操作,它在现在GUI框架中是一个有特色的机制,但是它的实现就没有那么简洁,但是它还要这样做就是为了:
- 把触发用户操作的控件 和 处理用户的操作逻辑 解耦合。信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了自己,Qt的信号槽机制保证了信号与槽函数的调用。支持信号槽机制的类或者父类必须继承于QObject类。
- 也想起到多对多的效果,一个信号可以connect到多个槽函数上,一个槽函数也可以被多个信号connect。(实际开发中,这种情况极少)
这就是可以把多个信号和多个槽函数绑定到一起:
缺点:
与回调函数相比,信号和槽稍微慢⼀些,因为它们提供了更高的灵活性,尽管在实际应用程序中差别不大。通过信号调用的槽函数比直接调用的速度慢约10倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调用速度对性能要求不是非常高的场景是可以忽略的,是可以满足绝大部分场景。
七、信号和槽的断开
可以使用disconnect断开信号和槽的连接。但是大部分情况下,把信号和槽连接好就不用管了,主动断开的情况就是要把信号重新绑定到另一个槽函数上,如果不断开,这个信号发送就会触发两个槽函数。
- 点击按钮修改标题:
- 先点击断开原信号按钮,建立新连接,再点击修改标题按钮:
断开连接前,标题修改为旧标题,当点击下方pushButton后,上方原来的信号和槽断开了连接,重新绑定后,再点击上方pushButton,标题修改为新标题。
八、使用 Lambda 表达式来定义槽函数
Qt5在Qt4的基础上提高了信号与槽的灵活性,允许使用任意函数作为槽函数。
但如果想方便的编写槽函数,比如在编写函数时连函数名都不想定义,则可以通过Lambda表达式来达到这个目的。
Lambda表达式是C++11增加的特性。C++11 中的Lambda表达式用于定义并创建匿名的函数对
象,以简化编程工作。
Lambda表达式的语法格式如下:
[ capture ] ( params ) opt -> ret {
Function body;
};
局部变量引入方式 [ ]
[ ]:标识一个Lambda表达式的开始。不可省略。
- 由于使用引用方式捕获对象会有局部变量释放了而Lambda函数还没有被调用的情况。如果执行Lambda函数,那么引用传递方式捕获进来的局部变量的值不可预知。所以绝大多数场合使用的形式为:={}
- 早期版本的Qt,若要使用Lambda表达式,要在"pro"文件中添加:
CONFIG += C++11
使用 Lambda 表达式的方式填写槽函数。这里的功能是点击一下按钮,按钮就移动到相应位置:
上述代码传值捕获没问题,传引用捕获会崩溃。原因是button是局部变量(它指向的空间位于堆区,但其本身是一个局部变量的指针),构造函数结束后button变量即被销毁,生命周期结束,若传引用对这块非法空间进行访问就会造成程序崩溃。