一、引言
什么是信号和槽?
简单来说,信号和槽是Qt中实现一种对象间信息通信机制。它用于一个对象在特定事件发生时,可以通知另一个对象和其他多个对象做出响应,并且这些对象之间不需要知道彼此的具体细节,实现了低耦合。
二、信号和槽的基本概念
1、信号
在Linux中我们知道信号Siganl,是系统内部的通知机制,是进程间通信的方式。在Qt中的信号和Linux中的信号,虽然不是同一样的概念,但是确实有相似之处。
Qt中谈到信号,也是涉及到三个要素:
信号源:
由哪个控件发出的信号。
信号的类型:用户进行不同的操作,就可以触发不同的信号。(点击按钮,触发点击信号;在输入框中移动光标,触发移动光标的信号等)
信号的处理方式:注册信号处理函数,在信号被触发的时候自动调用执行。Qt中可以使用connect这样的函数,把一个信号和一个槽关联起来,后续只要信号一触发Qt就会自动的执行槽函数。
2、槽
什么是槽?
槽就是一个普通的成员函数。当与其连接的信号被发射时,它就会被自动调用
所谓的"槽函数"本质上也是一种"回调函数"。
下面我们来举个例子更加好理解信号和槽:
假设正在上大学的张三谈了一个女朋友,有一天张三发现自己女朋友的脸色不善,脑子疯狂的运转,发现自己并没有做错什么事呀,这时张三突然想到可能是女朋友一个月总有那么几天不舒服,然后张三便买了一份红糖水给自己女朋友。
根据上述的例子:
信号源: 张三的女朋友
信号类型: 脸色不善
信号处理方式: 脑子疯狂运转,想是不是自己做错了什么,并得出原因 -> 每个月总会有几天不舒服 -> 买红糖水
上述的操作过程中,是提前把不同的信号的处理方式都准备好了。一定是先把信号的处理方式准备好,再触发信号。在Qt中,一定要先关联信号和槽,然后再触发这个信号,顺序不能颠倒,否则信号就不知道如何处理了。
三、connect函数的基本用法
这个函数和Linux中TCP套接字中建立连接的函数没有任何关系,只是名字恰巧一样。
connect参数:

接下来我们来举一个例子:
界面上包含一个按钮,用户点击按钮则关闭窗口。

我们先看前两个参数:

connect要求这两个参数是匹配的,button的类型如果是QPushButton*,此时第二个参数的信号必须是QPushButton内置的信号(父类信号),不能是一个其他的类。
看后两个参数:

close是QWidget内置的槽函数,Widget也就继承了父亲的槽函数。
我们运行上述代码:

点击按钮:

针对上述,会有两个问题:
问题一: 我们怎么知道QPushButton有一个clicked信号呢?又是怎么知道QWidget有一个close槽呢?
我们查看相关的文档,发现并没有找到对应的线索
这个时候我们仔细看,上边的是父类,下边的是子类
Qt提供了好几种按钮,这些按钮之间存在一些"共性"内容,就把这些共性的内容,提取出来放到了QAbstractButton类里面。
我们就可以查找到了clicked信号
我们再找槽函数close
问题二:

结合我们之前写的样例:
我们发现第二个参数和第四个参数都是函数指针啊,而Connect中传的参数是char*指针。我们都知道C++中不允许你使用两个不同的指针类型,相互赋值。
我们查看文档:

这个函数声明是以前旧版本的Qt的connect函数声明,以前的版本中,传参的写法和现在其实也是有区别的。
此时给信号参数传参,要搭配一个SIGNAL宏,给槽参数传参,搭配一个SLOT宏。

上述这两个宏就可以将传入的函数指针转化成char *
其实Qt5开始,对上述写法做出了简化,不再需要写SIGNAL和SLOT宏了,给connect提供了重载版本。在重载版本中,第二个参数和第四个参数成了泛型参数,允许我们传入任意类型的函数指针了。

我们跳转到connect函数中,我们就会发现此时connect函数就带有了一定的参数检查功能。如果你传入的第一个参数和第二个参数不匹配,或者第三个参数和第四个参数不匹配,(不匹配指的是:第二个参数和第四个参数的函数指针,不是第一个和第三个参数的成员函数),此时代码就会报错。
四、自定义信号和槽
1、自定义槽
什么是自定义槽: 所谓的槽就是一个普通的成员函数。所谓的自定义槽函数,操作过程和自定义一个普通的成员函数没啥区别。
下面我们直接来举个例子:

前面我们还是按照上一个例子创建一个按钮,并连接,只是连接的时候槽函数是我们自定义的函数。
运行代码:
在以前的Qt版本中,槽函数必须要放到public/private/protected slots中

此处的slots是Qt自己扩展的关键字(不是C++标准中的语法),Qt里广泛使用元编程技术(基于代码,生成代码),qmake构建Qt项目的时候,就会调用专门的扫描器,扫描代码中特定的关键字,基于关键字自动生成一大堆相关的代码。
还有一种定义槽的方式:

这个窗口就列出了QPushButton中给我们提供的所有信号

在这里编写我们需要的代码即可。在Qt中,除了通过connect来连接信号槽之外,还可以通过函数名字的方式来自动连接。

我们仔细看一下(黄色框中内容)第一个指的是按钮的objectName,第二个指的是信号的名字。当函数名符合上述规则之后,Qt就能自动的把信号和槽给联系上了。

假设我们将上述的函数名改了再运行,会看到如下的报错。

Qt中调用这个函数的时候,就会触发上述自动连接信号槽的规则!正是在自动生成ui_widget.h中调用的。
小结:
如果我们通过图形化界面创建控件,还是推荐上述第二个方式来快速连接信号槽。
如果我们是通过代码的方式创建控件,还是得手动connect。
2、自定义信号
上述我们讲了自定义槽,在开发中大部分情况都是需要自定义槽函数的。槽函数就是用户触发某个操作之后,要进行的业务逻辑。
Qt中也是允许自定义信号的,但是比较少见,实际开发中很少会需要自定义信号。信号就对应到用户的某个操作。 在GUI,用户能够进行哪些操作都是可以穷举的,Qt内置的信号,基本上已经覆盖到了上述所有可能的用户操作。因此使用Qt内置的信号就足以应付大部分的开发场景了。
在Widget中虽然还没有定义任何信号,由于继承自QWidget和QObject,这两类里面已经提供了一些信号了,可以直接使用。
所谓的Qt信号,本质上也就是一个"函数",Qt5以及更高版本中槽函数和普通的成员函数之间,没啥差别了。但是,信号则是一类非常特殊的函数:
1、程序员只要写出函数声明,并且告诉Qt,这是一个"信号"即可。这个函数的定义是Qt在编译过程中,自动生成的。也就是说信号在Qt中是特殊的机制,Qt生成的信号函数的实现,要配合Qt框架做很多既定的操作。
2、作为信号函数,这个函数的返回值必须是void。有没有参数都可以,甚至也可以支持重载。

这个也是Qt自己扩展出来的关键词,在qmake的时候,调用一些代码的分析/生成工具,扫描到类中包含signals这个关键字的时候,此时就会自动的把下面的函数声明认为是信号,并且给这些信号函数自动的生成函数定义。

如此,我们运行发现,为啥没有变化呢?
注意:建立连接不代表信号发出去了!
如何才能触发发出自定义信号呢?
Qt内置的信号,都不需要我们自动通过代码来触发,用户在GUI进行某些操作,就会自动触发对应的信号(发射信号的代码已经内置到Qt框架中了)
如下,加入emit便可以将我们的信号发出了

五、带参的信号槽
信号和槽也可以带参数,信号带有参数的时候,槽的参数必须和信号的参数一样。此时发射信号的时候就可以给信号函数传递实参,与之对应的这个参数就会被传递到对应的槽函数中。此时就可以起到让信号给槽传递的效果了。

这里的参数的类型必须一致,参数的个数不一致也可以,但是要求信号的参数个数必须要比槽的参数个数要更多。需要注意的是C++中声明函数的时候,形参的名字可以不必写。 但是下边的例子我还是会写上。

传参可以起到复用代码的效果,有多个逻辑,逻辑上整体一致,但是涉及到的数据不同的时候,就可以通过函数-参数来复用代码,并且在不同的场景中传入不同的参数即可。
Qt中有很多内置信号,也是带有参数的(这些参数不是我们自己传递的),例如clicked信号就带有一个参数。

这个参数表示当前是否处于"选中"的状态,这个选中的状态对于QPushButton来说没什么意义,对于QCeckBox复选框来说就很有用。这个就是我们平常看到的勾选选项,你需要的就点击框框就会打勾,不需要就再点击这个勾就会消失。
接下来我们增加信号的参数,槽函数的参数不变,看看会发生什么状况。


运行一下:

这个时候我们发现代码是可以正常的运行的,所以我们就可以得出,信号函数的参数个数,超过了槽函数的参数个数,此时都是可以正常使用的。
那反过来呢?槽函数的参数比信号函数的参数多呢?


运行一下:

综上所述,信号函数的参数个数,少于槽函数的参数个数,此时代码无法编译通过。
直观的思考,应该是要求信号的参数个数和槽的参数个数严格一致才对啊?!为啥此处允许信号的参数比惨函数的参数多呢?
一个槽函数可能会绑定多个信号,如果我们严格要求参数个数一致,就意味着信号绑定到槽的要求就变高了。换而言之,当下这样的规则,就允许信号和槽之间的绑定更灵活了,更多的信号绑定到这个槽函数上了。
个数不一致,槽函数就会按照参数顺序,拿到信号的前N个参数,至少需要确保槽函数的每个参数都是有值的。
我们再改一下代码:

由此我们可以知道,带有参数的信号,要求信号的参数和槽的参数要一致。类型、个数要满足要求(信号的参数个数要多于槽的参数个数)。

Qt中如果要让某个类能够使用信号槽(可以在类中定义信号和槽函数),则必须要在类最开始的地方,写下Q_OBJECT这个宏。这是Qt中的硬性规定。
这个宏能展开成很多额外的代码:

这里的宏还能进一步展开,最终展开的效果会得到一系列很复杂的代码,这些代码就涉及到Qt实现的内部原理了。
如果不加这个宏,这个类在编译的时候就会出错。

六、信号和槽存在的意义
所谓的信号槽,终究要解决的问题就是响应用户的操作。信号槽其实在GUI开发的各种框架中,是一个比较有特色的存在。
它们存在的意义有:
- 解耦通信双方: 在传统的编程模式中,如果两个对象之间需要通信,往往需要直接调用对方的方法。例如,一个按钮对象要通知一个文本框对象用户点击了按钮,可能会直接调用文本框对象的某个方法。但这种方式会使按钮对象和文本框对象紧密耦合。信号和槽机制就很好地解决了这个问题。按钮对象发出一个信号,而文本框对象通过连接这个信号到自己的槽函数来接收通知。这样,按钮对象不需要知道文本框对象的内部实现细节,文本框对象也不需要知道按钮对象是如何发出信号的,它们之间只是通过信号和槽进行松耦合的通信。
- 支持多对多通信: 一个信号可以连接到多个槽函数。比如在一个复杂的图形用户界面(GUI)程序中,一个窗口的关闭信号可能需要通知多个对象进行清理工作,如保存数据到文件、释放资源等。通过信号和槽机制,这个关闭信号可以同时连接到多个对象的槽函数,每个槽函数都可以执行自己相应的清理任务。同时,一个槽函数也可以接收多个信号。例如,一个槽函数可以同时处理来自不同按钮的点击信号,根据信号的来源来执行不同的操作。
- 实现异步通信: 在 Qt 的多线程环境中,信号和槽机制可以用于线程间的安全通信。当一个线程中的对象发出信号时,Qt 的事件循环机制可以将这个信号安全地传递到其他线程中的对象的槽函数中。例如,在一个后台线程中处理数据下载任务,当下载完成时,该线程中的对象发出一个信号,主线程中的对象通过槽函数接收这个信号并更新用户界面。这种方式避免了直接在不同线程之间操作共享数据可能带来的线程安全问题。
七、补充知识(disconnect和lambda)
1、使用disconnect来断开信号和槽的连接
看下列样例:

首先先按下第一个按钮,我们可以看到窗口标题被修改了。

我们点击下边的按钮,再次点击上边的按钮,我们会发现窗口的标题发生了改变。
2、定义槽函数的时候,使用lambda表达式
接下来,还是看一下下边的样例:

所以我们发现lambda表达式也是可以使用的。因为在很多的编程语言中都支持lambda表达式这种写法,所以它也称作"语法糖"。其实本质就是一个"匿名函数",主要应用在"回调函数"场景中,一次性使用。
如下操作我们发现出现了报错:

原因是lambda表达式是一个回调函数,这个函数是无法直接获取到上层作用域中的变量的。
lambda为了解决上述的问题,引入了"变量捕获"语法,通过变量捕获获取到外层作用域中的变量。

如果当前lambda里面想使用更多的外层变量怎么办?

如此,这个写法的含义就是把上层作用域中的所有的变量名都给捕获进来。
这里需要注意:lambda语法是C++11中引入的,对于Qt5及其更高版本,默认就是按照C++11来编译的,如果使用Qt4或者更老的版本就需要手动在.pro文件中加上C++的编译选项。









