目录
自定义信号
带参数的信号和槽
信号和槽存在的意义
信号与槽的连接方式
一对一
一对多
多对一
意义
信号和槽的其他说明
信号和槽的断开
使用Lambda表达式定义槽函数
信号与槽的优缺点
优点: 松散耦合
缺点: 效率较低
自定义信号
自定义槽函数是非常关键的,在开发中大部分情况都是需要自定义槽函数的。
槽函数,就是用户触发某个操作之后,要进行的业务逻辑
自定义信号,比较少见,在实际开发中很少会需要自定义信号。
信号就对应到用户的某个操作。
在GUI,用户能够进行哪些操作,是可以穷举的,在Qt中内置的信号,基本已经覆盖到了上述所有可能的用户操作。因此使用Qt内置的信号,旧足以应付大部分的开发场景。
widget这个类中虽然没有定义任何信号,由于继承自Qwidget和QObject,这俩个类里面已经提供了一些信号,可以直接使用。
所谓的Qt信号,本质上就是一个”函数“。
Qt5已经更高版本中,槽函数和普通的成员函数之间已经没有区别;但是信号则是一类特殊的函数:
- 只需要写出函数声明,并且告诉Qt这是一个”信号“即可,这个函数的定义,是Qt在编译过程中,自动生成的(自动生成的过程,程序员无法进行干预)。因为信号在Qt中式特殊的机制,Qt生成的信号函数的实现,要配合Qt框架左很多既定的操作。
- 作为信号函数,这个函数的返回值,必须式void,有没有参数都可以,甚至也可以支持重载。
这个signals也是Qt自己扩展出来的关键字,在qmake的时候,调用一些代码的分析/生成工具,扫描到类中包含signals这个关键字的时候,此时,就会自动的把下面的函数声明认为是信号,并且给这些信号函数自动的生成函数定义。
#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();
signals:
void mySignal();
public slots:
void handleMySignaal();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(this, &Widget::mySignal, this, &Widget::setWindowTitle);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleMySignaal()
{
this->setWindowTitle("自定义信号");
}
建立好联系之后,还需要将信号发送出去。
对于Qt内置的信号,都不需要手动通过代码来触发,用户在GUI进行某些操作,就会自动触发对应信号(发射信号的代码已经内置到Qt框架中了)。
关键字:emit 发射
发射信号的操作,可以在任意合适的代码中。
这样就可以完成自定义信号了。
也可以通过点击按钮的方式,进行对窗口的编辑。
【注意】其实在Qt5中emit什么都不会做,真正进行操作的都包含在mysignal内部生成的函数定义里面,所以可以舍弃emit,信号也能发送出去,即使如此,在实际开发过程中,建议把emit加上,这可以让代码的可读性更高,更加明显的标识出这里发射自定义的信号。
带参数的信号和槽
Qt的信号和槽也支持带有参数,同时也可以支持重载。
此处要求:当信号带有参数的时候,槽的参数必须和信号参数一致,当发射信号的时候,就可以给信号函数传递实参,与之对应的这个参数就会被传递到槽函数里,此时就可以起到让信号给槽传参效果了。
【注意】在C++中声明函数的时候,形参的名字可以不用写。
信号函数和槽函数必须一致,个数如果不一致也可以,当个数不一致的时候,要求信号参数的个数必须比槽的参数个数多。
传参可以起到复用代码代码的效果,如果有多个逻辑,逻辑上整体一致,但是涉及到的数据不同,就可以通过函数-参数来复用代码,并且在不同的场景中传入不同的参数。
通过这一套信号槽,搭配不同的参数,就可以起到设置不同标题的效果。
在Qt的很多内置的信号,也是带有参数的,这些参数不是咱们自己传递的。
例如:clicked信号就带有一个参数。
clicked(bool),这个参数表示当前按钮是否处于“选中”状态。这个选中状态对于QPushButton没有意义,但是对于QCheckBox复选框是很有用的。
信号函数的参数个数,超过了槽函数的参数个数,还是可以正常使用的。
如果信号函数的参数个数少于槽函数的参数个数,此时代码无法编译通过。
为什么允许信号的参数比槽的参数多呢?
一个槽函数,又可能会绑定多个信号。如果严格要求参数个数一致,就意味着信号绑定槽的要求就变高了。换而言之,当下这样的规则,就允许信号和槽之间的绑定更灵活了,有更多的信号可以绑定到这个槽函数上。
如果个数不一致,槽函数就会按照参数顺序,拿到信号的前N个参数。至少要确保,槽函数的每个参数都是有值的。所以要求信号给槽的参数,可以有富裕,但是不能少。
【注意】带有参数的信号,要求信号的参数和槽的参数一致,这里的一致指的是类型一致,个数满足要求(信号的参数个数要对于槽的参数个数)。
在Qt中,如果要让某个类能够使用信号槽(即可以在类中定义信号函数和槽函数),则必须在类最开始的地方,写下Q_OBJECT宏。
这个事情可以看作是Qt中的硬性规定,这个宏能展开成很多额外的代码。
这里的宏还可以进一步展开,最后展开的效果会得到一系列很复杂的代码,此处就不深入研究了。
如果不加这个宏,代码在编译期间就会出错。
信号和槽存在的意义
所谓的信号和槽,最终要解决的问题,就是响应用户的操作。
信号与槽的连接方式
一对一
主要有俩种形式,分别是:一个信号连接一个槽和一个信号连接一个信号
一对多
一个信号连接多个槽
多对一
多个信号连接一个槽函数
意义
一个信号,可以connect多个槽函数,一个槽函数也可以被多个信号connect。
Qt引入信号与槽机制,最本质的目的就是为了能够让信号和槽之间按照“多对多”的方式进行关联。其他GUI框架是不具备这样的特性。但是,随着程序开发的经验变多,在GUI开发过程中,多对多这种情况,其实是”伪需求“,在实际开发中很少用到,绝大部分的情况,一对一就够用了。
信号和槽的其他说明
信号和槽的断开
使用disconnect即可完成断开,disconnect的用法和connect基本一致。
实际使用中,disconnect的使用比较少,大部分情况下把信号和槽连接上之后,就不用等了。
主动断开的目的往往是把信号重新绑定到另一个槽函数上。
- 断开原来的信号槽
- 重新绑定信号槽。
如果没有disconnect,就会构成一个信号绑定俩个槽函数,触发信号的时候,俩个槽函数都会执行。
使用Lambda表达式定义槽函数
Qt5在Qt4的基础上提高了信号与槽的灵活性,允许使用任意函数作为槽函数。但是如果想要方便的编写槽函数,例如在编写函数的时候连函数名都不想定义,就可以通过Lambda表达式来达到这个目的。
Lambda表达式是C++11增加的特性。在C++11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。
Lambda表达式的语法格式如下:
其本质上就是一个”匿名对象“,主要应用在”回调函数“场景中。
lambda表达式是一个回调函数,引入了”变量捕获“语法,通过变量捕获,获取到外层作用域中的变量。
在[ ]可以添加下面函数中需要的参数,如果想要使用外层的全部变量就需要[=]。
[=]这个写法的函数就是把上层作用域中的所有变量都给捕获进来。
#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->move(350, 250);
connect(button, &QPushButton::clicked, this, [=](){
this->setWindowTitle("lambda");
button->setText("lambda");
});
}
Widget::~Widget()
{
delete ui;
}
如果对应的槽函数比较简单,而且是一次性使用的,经常会写成lambda形式。
另外也需要确认捕获到lambda内部的变量是由意义的,例如:无论何时用户点击了按钮,捕获到的变量都能正确使用。
信号与槽的优缺点
优点: 松散耦合
信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了⾃⼰,Qt的信号槽机制保证了信号与槽函数的调⽤。⽀持信号槽机制的类或者⽗类必须继承于QObject类。
缺点: 效率较低
与回调函数相⽐,信号和槽稍微慢⼀些,因为它们提供了更⾼的灵活性,尽管在实际应⽤程序中差别不⼤。通过信号调⽤的槽函数⽐直接调⽤的速度慢约10倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调⽤速度对性能要求不是⾮常⾼的场景是可以忽略的,是可以满⾜绝⼤部分场景。