目录
概述
信号和槽的使用
自定义信号和槽
带参数的信号和槽
概述
在Linux系统中,我们也介绍了信号的产生、信号的检测以及信号的处理机制,它就是系统内部的通知机制,也可以是一种进程间通信的方式。在系统中有很多信号,我们可以通过signal()函数捕捉信号,重写一个信号处理函数。在Qt中的信号也和系统中的信号有相似之处。
Qt中的信号也要涉及信号三要素:信号源、信号类型和信号处理方式。
- 信号源:Qt中的信号是由某个控件发出的,Linux系统中可以是一个进程发送的信号。
- 信号类型:用户不同的操作会触发不同的信号,例如点击按钮就会触发点击信号(clicked)、在输入框中移动光标也会触发相应的信号,我们写这样的GUI程序就是为了和用户交互,所以必须知道当前用户的具体操作,通过不同的操作进行不同的处理。
- 信号处理方式:在Qt中就引入了一个概念就是槽(slot),说白了就是一个函数,再使用connect这样的函数把一个信号和一个槽关联起来,之后只要触发了信号,Qt就会自动执行槽函数,这种槽函数本质上也是一种回调函数。
所以在Qt中一定要先关联信号和槽,也就是先要有槽函数并使用connect函数将二者关联起来,然后再触发这个信号,顺序不能颠倒。
信号和槽的使用
在Qt中,QObject类提供了一个静态成员函数,就是connect(),这个函数就是关联信号和槽函数的。
而且Qt中的类本身也存在着一定的继承关系,比如我们使用的QLabel、QPushButton、QLineEdit等,他们的父类都是QWidget,也就是我们创建项目时选择的要继承的类,这个QWidget类也是继承了QObject这个类。所以Qt中基本所有的类都可以使用connect()这个函数。
QMetaObject::Connection QObject::connect(const QObject *sender,\ const char *signal,\ const QObject *receiver,\ const char *method,\ Qt::ConnectionType type = Qt::AutoConnection)
这个函数上一篇我们已经介绍过了,再来说一下细节:
- 前两个参数必须要匹配,比如是QPushButton对象发出的,那也必须是QPushButton内置的信号。
- 第一个和第三个参数我们传入的是QObject的子类指针,所以没有问题,但是第二个和第四个参数传入的是函数指针,但是为什么使用char*来接收呢?这两个的意义可不一样.
- 原因就是这是旧版本的connect函数声明,函数传参是时候需要给这两个参数分别放在SIGNAL()和SLOT()的括号中,通过宏替换就可以把函数指针转换成char*.
- 所以从Qt 5开始,做出了简化,不需要写这两个宏了,给connect函数提供了重载版本。
template <typename Func1, typename Func2> static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender,\ Func1 signal,\ const typename QtPrivate::FunctionPointer<Func2>::Object *receiver,\ Func2 slot,\ Qt::ConnectionType type = Qt::AutoConnection)
- 这就是为什么传参的时候一定要匹配,这样也有了一定的参数检查功能。
我们再写一个场景来使用一下。
// 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->setText("关闭"); button->move(300, 200); connect(button, &QPushButton::clicked, this, &Widget::close); } Widget::~Widget() { delete ui; }
- 创建一个QPushButton对象,想要点击这个按钮后就关闭这个窗口,发出信号的一定是QPushButton对象。
- 发出的信号一定是QPushButton内置的clicked信号。
- 要交给的对象就是this也就是Widget对象。
- 处理函数就可以使用Widget继承的QWidget内置的close()槽函数。
运行后就可以通过点击这个按钮关闭这个界面了。
但是像clicked这样的信号还有多少呢,想要知道就要看看文档了。
我们打开这个程序就可以查看文档,在文档的索引框,查找QPushButton类。
找了一遍后发现没有找到clicked这样的信号,既然这样就要向它的父类中查找。
QPushButton的父类就是QAbstractButton,通过类名我们也可以得知这是一个抽象类,在这个页面向下查找就找到了Signals这块,这里就有clicked等信号,也就类似于函数。
自定义信号和槽
有了上述的介绍,我们现在就可以自己写一个信号处理动作,这也是上一篇也用过的,先使用纯代码的方式实现一下自定义槽。
- 还是要先new一个QPushButton对象,把点击信号和槽函数关联起来。
- 槽函数中就设置为,捕捉到了信号就把Widget界面的标题修改一下。
下面我们再来看一下通过图形化界面的方式设置信号和槽。
- 打开Qt Disigner,通过拖拽的方式把Push Button控件添加到界面。
- 可以编辑一下按钮文本,然后右键点击控件,再点击转到槽。
- 点击后就可以看到Push Button提供的信号,甚至还有它的父类的信号,常用的还是clicked信号。
- 双击clicked就会跳转到编辑widget.cpp的界面,此时函数的声明和定义都已经自动生成好了,编写代码即可。
【注意】:这种方式是不会出现connect()函数的,Qt中除了connect可以连接信号和槽外,还可以通过函数名字的方式来自动连接,如上图所示:
- on作为固定前缀。
- pushButton为控件的objectName。
- clicked为信号名字。
当符合规则后,Qt就可以把信号和槽自动建立好,所以函数名必须是正确的,如果不正确就无法连接。
这里名字故意写错,就不能达到我们想要的效果,这都是Qt中调用connectSlotsByName这个函数触发自动连接信号和槽,这个函数是在自动生成的ui_widget.h中setupUi函数中调用的。
所以,如果是通过图形化界面创建控件,还是使用第二种方式快速连接信号和槽;如果是通过代码的方式创建控件,还是得手动调用connect函数,原因就是没有调用connectSlotsByName函数。
虽然Qt中允许自定义信号,但是比较少见,开发过程中很少自定义信号,在GUI界面的操作是可以穷举出来的,Qt中内置的信号基本上已经覆盖了,所以使用Qt内置的信号就足够了。
Widget类虽然没有定义任何信号,但是它继承了QWidget和QObject,所以他们两个的信号可以直接使用。
信号本质上也是一个函数,它是是一个特殊的函数,只需要写出函数声明,告诉Qt这是一个信号即可,这个函数的定义在编译过程中是自动生成的。
而且信号在Qt中是特殊的机制,Qt生成的信号函数的实现也要配合Qt框架做其他既定操作。
作为信号函数,返回值必须是void,有无参数都可以,也可以支持重载。
signals是Qt自己扩展的关键字,qmake会调用代码分析和生成的工具,识别到signals这个关键字时就会把下面的函数声明认为是信号,并自动生成函数定义。
通过connect函数连接信号和槽函数就可以了,但是光连接还是不够的,还需要发送我们自定义的信号,这个emitu也是Qt中的关键字,作用就是发送信号。
或者我们把信号从构造函数拿出来,当点击按钮,就会触发on_pushButton_clicked函数,函数中就会发送mySignal信号,收到信号就会执行handleSignal自定义函数,之后就会修改窗口标题。
带参数的信号和槽
上述的不管是我们自定义的还是Qt内置的信号和槽都是不带参数的,但是他们也可以带参数,并且信号和槽的参数必须一致,此时触发信号的时候,就可以给信号函数传递实参,这个参数就会被传递到对应的槽函数中。
参数的类型必须要一致,个数不一致是可以的,但是要求信号的参数的个数必须要比槽的参数多。
通过这一次连接信号和槽,并搭配不同参数就可以实现不同结果,可以让代码复用,就比如:
而且信号的参数比槽函数多也是可以的,但是不允许槽函数中的参数比信号中的多,原因就是:
- 一个槽函数可能绑定多个信号。
- 如果严格要求就意味着信号绑定到槽的要求变高了。
- 所以信号的参数个数可以大于槽的参数个数,这样让信号和槽之间的绑定变得更灵活,更多的信号可以绑定到这个槽函数上。
- 虽然个数不一致,但是槽函数会按照参数顺序拿前N个参数,这样就可以确保槽函数的每个参数都有值。
还有一点就是,如果想要使用信号和槽的机制,就必须在类的一开始写上Q_OBJECT宏。
当我们转到定义就可以看到这个宏中展开的代码,这里也有很多的宏,也会继续展开,最终实现相应的功能。
信号和槽最主要就是要解决响应用户的操作,它在GUI框架中是一个有特色的机制,但是它的实现就没有那么简洁,但是它还要这样做就是为了:
- 把触发用户操作的控件 和 处理用户的操作逻辑 解耦合。
- 也想起到多对多的效果,一个信号可以connect到多个槽函数上,一个槽函数也可以被多个信号connect。
这就是可以把多个信号和多个槽函数绑定到一起。
【补充】:
- 可以使用disconnect断开信号和槽的连接。但是大部分情况下,把信号和槽连接好就不用管了,主动断开的情况就是要把信号重新绑定到另一个槽函数上,如果不断开,这个信号发送就会触发两个槽函数。断开连接前,标题修改为旧标题,当点击下方pushButton后,上方原来的信号和槽断开了连接,重新绑定后,再点击上方pushButton,标题修改为新标题。
- connect函数连接的槽函数也可以是一个lambda表达式。还可以这样使用。但是要注意lambda表达式的捕捉条件,“=”是值的方式捕捉,“&”是引用的方式,要是使用引用的方式就要注意这个变量的生命周期。