什么是信号与槽?
“信号”这个词在我们的日常生活中随处可见,比如:信号灯变绿,我们行人进行通行、鸡叫表示天亮了、下课铃响了代表着下课了、王者连跪表示我们该充钱了等等信号这个概念,在我们的生活中随处可见。从以上的例子中我们仔细观察的话其实是会发现,每一个信号后面都对应着我们的一个动作,比如:绿灯了,我们要通过人行道、鸡叫了我们要起床了、下课了我们要去干饭、王者连跪了我们要打开微信充钱…这些信号后面伴随的动作是怎么来到?或者说我们怎么知道接收到这些信号后该做什么?当然是生活经验、和老师学校教的反正不可能是临时起意嘛!换而言之就是这些信号后面代表的动作都是我们早已知道的!
那么在QT中也是同理,信号就代表着用户发出了一个指令,而槽就代表着对于这个指令的处理方法!
其中,信号的三要素包括:发送源、信号类型、处理方法;
在QT开发中,信号和槽都是通过函数来进行表现的;
connect
上面既然说了,信号的三要素是:发送源、信号类型、处理方法,可是对于QT来说,它怎么知道哪一个信号该调用哪一个处理方法呢?也就是说,对于QT来说,它怎么知道A信号该调用那个处理方法呢?
当然,是我们开发人员提前将信号与处理方法之间的关系绑定好,到时候QT直接进行调用即可!
而connect函数就是专门用来完成信号与处理方法直接的绑定的!
下面我们来看一看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函数实际上是QObject类提供的一个静态成员函数,该函数专门用来进行信号与处理方法(槽函数)之间的绑定!
关于QObject类:
QObject类实际上是QT中所有内置类型的祖宗类!也就是说在QT中大部分类要么直接继承自QObject类,要么间接继承自QObject类,与Java中的继承十分相似!
在QT中大概的继承关系如下(不准确的继承):
信号和槽的使用
- 我们在窗口上设置一个按钮,当我们按下这个按钮就可关闭窗口
分析: 当用户按下按钮控件过后,实际上是向QT发送了一个叫clicked的信号,为此我们现在要做的就是捕捉该信号,为该信号关联一个关闭函数,也就是对应Widget类中的close函数:
具体代码如下:
通过实现测试,我们发现,我们确实可以通过点击窗口上的按钮来关闭窗口;但是对以上的代码我们有两个疑问?
- 我们怎么知道点击一下按钮就会发出一个clicked信号?或者我们怎么知道QT有哪些信号或槽函数?
答:多看官方文档;当我们不了解某一个知识点的时候,官方文档是最好的学习工具,况且QT自己在也已经内置了官方文档;
如果我们要查找某一个类的信号的话,我们可以直接对这个类进行搜索,当然可能这个类中并没有包含整个信号,那么我们可以对这个类的父类、祖宗类进行搜索,这样的话我们一定可以找到这个信号,就比如上图的clicked信号,在QPushButton中就没有,但是在其父类QAbstractButton就存在;对于槽函数我们也是如此;
- 对于connect函数的第二个参数和第四个参数来说,他们明明是const char * 类型的为什么可以接收函数指针类型?(并且这在C++中是绝对禁止的!) 就拿上面connect的例子举例,clicked的类型应该是void ( * ) () ,而close的类型应该是bool ( * ) (),可是为什么这两个函数指针类型可以直接赋值给const char*?
答:在早期的QT中,这确实是不行,于是早期给出的解决方案就是在对于信号传参的时候需要用SIGNAL宏修饰一下,对于槽函数传参的时候需要使用SLOT宏修饰一下,这样就不会有类型不匹配的问题了,eg: connect(button1,SIGNAL(&QPushButton::clicked),this,SLOT(&Widget::close));
但是对于QT5以上的版本来说,这些版本都是基于C++11的语法来进行编译的,也算是吃到了版本红利,QT官方利用函数重载+模板技术重载了一个connect:
这样的话,对于一个函数指针来说,它就不用再使用SIGNAL宏和SLOT宏来进行修饰了,根据模板的特性,它会自动推导传递过来的函数指针的类型,极其方便!同时新版的connect还使用了“类型萃取”技术,它可以帮助我们检查,发送源控件与信号类型是否匹配,如果不匹配会发生编译报错!;
自定义槽函数
虽然QT已经给我们内置了许多对应的槽函数,但是在我们实际的开发中,我们更多的情况下,是结合自己的业务场景,针对对应的信号量身定做一个槽函数,也就是自定义槽函数;
自定义参函数的语法:
- 自定义一个槽函数的本质就是定义一个成员函数;
- 该成员函数需要使用public signals、private signals、protected signals来进行修饰;(在QT 5及以上版本中可以不用signal修饰)
为此,为了演示自定义槽函数,我们设计出一个具体的场景:
- 当用户点击按钮的时候就可以切换窗口标题;
分析: 当用户点击按钮的时候,实际上会发出一个clicked信号,现在对应的处理动作是将窗口标题改为:“Hero1234567”,为此我们需要捕捉clicked信号,并且自定义处理函数:
具体代码如下:
运行结果如下:
以上场景的实现模式是使用C++纯代码的方式来进行实现的,但是通过对于QT的学习我们知道,实际对于QT开发,我们不仅有C++纯代码的方式,还可以通过图形化的方式来进行实现,为此我们通过图形化的方式来实现一下以上场景:
小插曲:在上图种我们可以看到右边有一个叫做objectName的属性,这个属性用来唯一标识左边窗口上的控件,不能重复,一般情况下有QT自己为我们分配,当然我们也可以自定义,前提条件是不能重复!因为我们在代码中如果想要访问通过这种方式创建出来的控件的话实际上就是通过ui->objectName来进行访问的!因此我们绝对不能将控件的唯一标识进行重复:
运行结果如下:
总结:
- 通过以上两种实现方式,我们可以发现,将信号与槽函数绑定的方式有两种:第一种就是我们手动使用connect函数来进行绑定;第二种就是我们使用图形化界面创建出来的控件和槽函数,这种方式下,QT内部会自动帮助我们完成connect,无需我们用户手动完成!
- 通过图形化方式创建出来的控件,有一个很重要的唯一的属性,也就是objectName,该值相当于这个控件的唯一标识,我们后续如果想要在代码中访问这些控件,我们可以通过ui->objectName的方式来进行访问:
自定义信号
实际上,作为一个GUI程序,QT几乎穷举了用户的所有操作,也就是说QT穷尽了用户可能发出的绝大多数信号,这些内置信号就够我们大部分的开发场景使用了,当然我们也说了只适用与大部分场景,对于某些场景还是需要我们使用自定义信号的!
自定义信号的语法:
- 自定义信号本质就是自定义一个函数声明;
- 这个函数声明需要使用slots关键字进行修饰;
- 这个函数声明可以带参数,但是返回值必须是void;
为此,我们根据该场景设计出一个具体的业务场景:
- 当用户点击按钮就可以切换窗口标题;
设计思路: 用户点击按钮,会发出clicked信号,我们捕捉这个信号,并于handleClicked槽函数绑定,这个槽函数的功能就是再次发出mySignal信号,然后我们再次捕捉该信号,并用一个自定义的槽函数与之绑定,该槽函数的功能就是切换窗口标题:
具体代码如下:
实际上在发送mySignal信号那一步,可以不叫emit关键字,因为在QT中发送信号的本质就是调用这个信号!加上emit是为了代码的可读性,表示这是一个发送信号的操作;
运行结果如图:
带参数的信号和槽
实际上,QT的大多数信号和槽函数都是不带参数的,但是如果我们想要我们的信号带上参数,也不是不可以,QT是支持我们这么干的!但是有几点要求要注意:
- 在自定义信号和槽函数的时候可以带上参数,并且QT允许重载信号和槽函数;
- 信号在和槽函数进行绑定的时候,槽函数的参数必须和它绑定的信号的参数完全一致!这个一致主要表现在以下两点:
2.1 槽函数的参数类型必须和它绑定的信号的参数类型一致;
2.2 槽函数的参数个数可以少于等于它绑定的信号的参数的个数; - 我们发送信号时给信号传递的参数,也会被QT当作参数传递与该信号绑定的槽函数;一般情况下QT内置的信号的参数传递,都是由QT自己完成;当然我们用户也可以手动的使用emit关键字来发送QT内置信号;
为此,我们可以针以上信息,创建出一个具体场景:
- 还是用户点击按钮,切换窗口标题的;
设计思路: 这次我们设计一个带参数的自定义函数,我们在发送这个信号的时候,可以给它传递一个字符串;当用户点击按钮的时候就会触发clicked信号,该信号又回去调用handleClicked槽函数,该槽函数会发送一个mySignal信号,并且在mySignal信号上传递一个字符串;当mySignal信号被发送出去过后,QT会调用handleMySignal槽函数来进行处理,该槽函数可以接收到来自mySignal信号传递过来的字符串,它会根据这个字符串来切换窗口标题;
具体代码:
当然,mySignal信号绑定无参的槽函数也可以:
总结
- 如果一个自定义类中想要使用信号和槽机制的话,那么我们必须在这个类的首行加Q_OBJECT宏:
否则会出现编译错误:
2. 在QT中一个信号可以绑定多个槽函数,一个槽函数可以绑定多个信号;
eg:
设计思路: 现在我们自定义两个信号:
一个void mySignal(const QString&);
一个void mySIgnal2();
然后在自定义两个槽函数:
void handleMySignal(const QString&)
void handleNoneMySignal();
现在我们在窗口上创建一个按钮,当用户点击这个按钮的时候就会触发clicked信号,也就会调用handleClicked槽函数,这个槽函数的作用就是发送mySignal、和mySignal2信号;
现在我将mySignal信号绑定handleMySignal和handleNoneMySignal这俩个槽函数;
将mySignal2信号绑定handleNoneMySignal这个槽函数:
因此,现在当我们点击按钮后会,发现控制台上打印handleMySignal一次、打印handleNoneMySignal两次
具体代码:
- 定义槽函数的时候可以使用Lambda表达式(如果想要捕获某个控件,建议使用值捕获):
eg:
运行结果:
当然在使用lambda表达式的时候,我们尽量使用值捕获,不要使用引用捕获,因为信号什么时候触发我们是不确定的,如果我们采用引用捕获,那么当我们离开了那个存储控件指针的变量的作用域时,才触发信号,就比如上面的button1变量,如果在我们离开Widget构造函数过后,刚好触发mySignal2信号,那么此时引用的button1变量就是个非法空间!那么这时候对这块空间进行访问就会出现段错误!!! - 信号与槽函数进行绑定过后,可以断开连接,重新进行绑定;
断开连接的接口是disconnect;
bool disconnect(const QObject *sender,
const char *signal,
const QObject *receiver,
const char *member);
参数设计上与connect函数完全一样;
设计一个场景:
现在窗口上有两个按钮,A、B当用户点击A按钮就可将窗口标题变为A;
当用户点击B按钮就可以将窗口标题变为B;
设计思路:
首先自定义一个void mySignal()信号
使用图形化界面生成两个按钮和对应的槽函数:on_A_clicked、on_B_clicked;
在自定义两个槽函数:void switchTitleToA()、void switchTitleToB();
在on_A_clicked()槽函数中建立mySignal与switchTitleToA的关系,然后发射mySignal信号,然后取消mySignal信号与switchTitleToA的联系;
在on_B_clicked()槽函数中建立mySignal与switchTitleToB的关系,然后发射mySignal信号,然后取消mySignal信号与switchTitleToB的联系;
具体代码如下: