目录
关于信号
关于信号在 Linux 中也学过信号的概念,Qt中的信号与 Linux 中的信号是不同的概念,但是也是有相似之处的
Qt 中,谈到信号,也是涉及到三个要素:
①信号源
由哪个控件发出的信号
②信号的类型
用户进行不同的操作,就可能触发不同的信号,例如:
点击按钮,触发点击信号
在输入框中移动光标,触发移动光标的信号
勾选一个复选框、选择一个下拉框都会触发出不同的信号
③信号的处理方式
槽(slot) => 函数
Qt 中可以使用 connect 这样的函数,把 一个信号和一个槽 关联起来,后续只要信号触发了,Qt 就会自动的执行槽函数
所谓的"槽函数"本质上也是一种"回调函数"(callback)
Qt 中一定是先关联好 信号和槽,然后再触发这个信号,顺序不能颠倒,否则信号就不知道如何处理了
connect函数
这个函数和 Linux TCP socket 中建立连接的函数,没有任何关系,只是名字恰巧一样
关于connect
connect函数是 QObject 提供的静态的成员函数,Qt 中提供的这些类,本身是存在一定的继承关系的:
QObject 就是其他 Qt 内置类的 祖宗
正是因为 connect函数 是 QObject 提供的,而其他的类都是继承自 QObject 的,所以其他的类也可以使用 connect函数
connect的使用
connect函数的声明:
connect (const QObject *sender,
const char * signal ,
const QObject * receiver ,
const char * method ,
Qt::ConnectionType type = Qt::AutoConnection )
上述的 connect函数 总共有5个参数,但是第5个参数提供了一个默认参数,所以我们可以暂时不考虑,大部分情况下使用的前面4个参数
-
参数1 sender:描述了当前信号是哪个控件发出来的
-
参数2 signal:发送的信号类型
-
参数3 receiver:哪个对象(控件)负责处理这个信号
-
参数4 method:这个对象怎么处理(要处理信号的对象提供的成员函数,槽函数)
下面是简单的示例,界面上包含一个按钮,用户点击按钮,则关闭窗口:在使用QPushButton时,传入第二个参数时,有两个提示,一个是click,另一个是 clicked
click:是一个 slot 函数,作用就是在调用的时候相当于点击了一下按钮
clicked:是过去分词形式,表示点完了才会触发点击信号
我们使用的是clicked,表示点击按钮再触发点击信号
connect 要求,第一个和第二个参数是匹配的,button 的类型如果是 QPushButton*
那么第二个参数的信号必须是 QPushButton 内置的信号(或是QPushButton 父类的信号),不能是一个其他的类,比如 QLineEdit 的信号
close 是 QWidget 内置的槽函数,Widget 继承自 QWidget,也就继承了父亲的槽函数
close 槽函数功能已经是人家内部实现好了,不需要咱们去关心
close 槽函数具体的作用就是:关闭当前的窗口/控件
widget.cpp实现如下:
此时运行项目:
此时点击这个按钮就会关闭窗口了
我们是怎么得知QPushButton中有clicked函数,Widget中有close函数的?
很简单,查看文档即可,如果在当前类中没有找到,那就在当前类的父类中寻找
还有一个问题,下面是我们在代码中使用的connect函数:
connect(button, &QPushButton::clicked, this, &Widget::close);
但是我们看connect的函数声明中,前4个参数的类型如下:
connect (const QObject *sender,
const char * signal ,
const QObject * receiver ,
const char * method )
其中第一个和第三个类型是 QObject* 的,我们传入的 button 和 this 都是 QObject 的子类的指针,能够理解
但是第二个和第四个参数,声明中是 char* 的,但是我们传入的是 &QPushButton::clicked 和 &Widget::close,这两个是函数指针,并不是 char* 类型,这是为什么呢?
其实这个函数声明,是以前版本的 Qt 的 connect 函数的声明,以前版本中,传参的写法和现在其实也是有区别的,以前的版本中:
给信号参数传参时,要搭配一个 SIGNAL 宏
给槽参数传参,搭配一个 SLOT 宏,也就是下面的这种形式:
connect(button, SIGNAL(&QPushButton::clicked), this, SLOT(&Widget::close));
从 Qt 5 开始,对上述写法做出了简化,不再需要写 SIGNAL 和 SLOT 宏了
给 connect 提供了重载版本,重载版本中,第二个参数和第四个参数成了 泛型参数,允许传入任意类型的函数指针
此时 connect 函数就带有了一定的 参数检査 功能
如果传入的第一个参数和第二个参数不匹配,或者第三个参数和第四个参数不匹配,此时代码编译出错
不匹配指的是:2/4 参数的函数指针,不是 1/3 参数的成员函数
自定义信号、自定义槽
在刚刚使用 connect函数 时,传入的 clicked 和 close 都是 Qt 内置的,但是内置的信号和槽不一定可以满足我们的要求,所以我们需要自定义信号和自定义槽
自定义槽
slot(槽) 其实就是一个普通的成员函数
自定义一个槽函数,操作过程和自定义一个普通的成员函数没啥区别
下面实现一个自定义的槽函数,在点击按钮后,就执行我们自己实现的槽函数 handleCilcked
在以前的 Qt 的旧版本中,自定义槽函数必须要写在 private/protected/public slots: 下面,后面的新版本就不需要这样了
slots 是 Qt 自己扩展出来的关键字
第一种方式自定义槽
widget.h 中声明:
widget.cpp 中定义:
此时运行程序:
点击按钮:
第二种方式自定义槽
使用图形化界面的方式,先将按钮拖到界面上:
右键按钮,点击转到槽:
这个窗口会列出 QPushButton 中给我们提供的所有的信号
还包含了 QPushButton 父类的信号:
-
QPushButton 继承自 QAbstractButton
-
QAbstractButton 继承自 QWidget
-
QWidget 继承自 QObject
所以下面将这几个父类的信号都列举出来了
选择 clicked,再点击OK,此时在 widget.h 和 widget.cpp 中就有了声明和定义,此时在该函数的内部编写函数的代码即可:
依旧是实现刚刚的功能:
此时运行程序:
点击按钮:
我们发现上述的第二种方式,并没有使用 connect函数:
在 Qt 中,除了通过 connect 来连接信号和槽之外,还可以通过函数名字的方式来自动连接
当函数名符合上述规则之后,Qt 就能自动的把信号和槽给建立上联系
Qt 中调用 connectSlotByName函数 的时候,就会触发上述自动连接信号槽的规则
connectSlotByName函数 正是在自动生成的 ui widget.h 中调用的
-
如果我们通过图形化界面创建控件,还是推荐使用这种快速的方式来连接信号槽
-
如果我们是通过代码的方式来创建控件,还是得手动 connect,因为你的代码中没有调用 connectSlotsByName函数
自定义信号
Qt 中也允许自定义信号
自定义槽函数是非常关键的,开发中大部分情况都是需要自定义槽函数的
槽函数:就是用户触发某个操作之后要进行的业务逻辑
自定义信号比较少见,实际开发中很少会需要自定义信号,信号就对应到用户的某个操作
在 GUI 中,用户能够进行哪些操作,是可以穷举的
Qt 内置的信号,基本上已经覆盖到了上述所有可能的用户操作
因此,使用 Qt 内置的信号,就足以应付大部分的开发场景了
所以这里的自定义信号只需简单了解即可
信号是一类非常特殊的函数
程序员只要写出函数声明,并且告诉 Qt,这是一个"信号"即可
这个函数的定义是 Qt 在编译过程中,自动生成的
自动生成的过程, 程序员无法干预,原因是:
信号在 Qt 中是特殊的机制,Qt 生成的信号函数的实现,要配合 Qt 框架做很多既定的操作
作为信号函数,这个函数的返回值必须是 void
有没有参数都可以,甚至也可以支持重载
signals 也是 Qt 自己扩展出来的关键字
qmake 的时候,调用一些代码的分析/生成工具,扫描到类中包含 signaks 这个关键字的时候,就会自动的把下面的函数声明认为是信号,并且给这些信号函数自动的生成函数定义
所以我们先在 widget.h 中声明自定义的信号函数 mySignal,和自定义槽函数 handleClicked :
widget.cpp中:
此时运行,发现窗口的标题并没有任何的改变,是因为我们这里只是使用 connect 建立连接了,并没有发出信号
如何才能触发出自定义的信号呢?
Qt 内置的信号,都不需要我们手动通过代码来触发,用户在 GUI 进行某些操作时,就会自动触发对应信号,例如 clicked 操作就是 点击按钮 后触发(发射信号的代码已经内置到 Qt 框架中了)
自定义类型的信号,就需要关键字 emit(发射) + 信号函数,就可以触发自定义的信号了
所以新增一行代码:
运行结果:
也就是当我们启动程序后,窗口启动好以后,信号就发送出来了,下面的槽函数就会进行触发
我们也可以不选择在构造函数中发送信号,可以选择点击按钮时再发送信号,所以采用 图形化 的方式,拖动一个按钮到界面上,再右键点击 转到槽:
点击 clicked,再点击 OK:
这里将发送信号的代码写到 on_pushButton_clicked函数 中:
此时运行程序:
点击 发送信号 按钮:
下面梳理一下代码的逻辑:
-
点击按钮,相当于按钮发送了一个 clicked 信号
-
clicked 信号关联到 on_pushButton_clicked槽函数
-
on_pushButton_clicked槽函数 执行又发送了 mySignal信号
-
mySignal信号 又会关联到 handleClicked槽函数
-
所以就会执行 handleClicked槽函数 中改变窗口标题的设置
其实在 Qt 5 中 emit 现在啥都没做,真正的操作都包含在 mySignal 内部生成的函数定义了
所以即使不写 emit,信号也能发送出去,但是还是建议加上 emit,可读性更强,能更明显的表示出什么地方是发射自定义信号
emit除了发射自定义信号,Qt内置的信号也能借助 emit 发射出来
信号槽
带参数的信号槽
我们刚刚所写的 信号和槽函数 都是没有参数的,但是 信号和槽函数 其实也是可以带参数的
当信号带有参数的时候,槽的参数必须和信号的参数一致
类型必须一致,参数个数可以不一致
此时发射信号的时候,就可以给信号函数传递实参,与之对应的这个参数就会被传递到对应的槽函数中,此时就可以起到让信号给槽传参的效果
参数个数一致的示例
我们将 widget.h 中自定义的 信号和槽函数 都加上一个相同的参数:
widget.cpp 中:
在发送信号时,信号中带的实参,就会被传递到槽函数对应的形参中,这样就完成了通过信号给槽传参的效果
此时运行程序:
点击按钮:
传参可以起到复用代码的效果
有多个逻辑,逻辑上整体一致,但是涉及到的数据不同,就可以通过函数-参数来复用代码,并且在不同的场景中传入不同的参数即可
如下所示,使用 图形化 的方式创建两个按钮,并分别右键点击 转到槽:
这两个槽函数分别传入 发送文本1 和 发送文本2 这两个信息:
运行程序:
点击第一个按钮:
点击第二个按钮:
此时就可以通过这一套信号槽,搭配不同的参数,就可以起到设置不同标题的效果
参数个数不一致的示例
此时在 widget.h 中,将信号参数多加一个,槽的参数保持不变:
在 widget.cpp 中,给信号传参时也加上第二个参数:
此时虽然槽函数只有一个参数,但是不影响程序的正确运行,和上述执行的情况是一样的
-
信号函数的参数个数,超过了槽函数的参数个数,此时都是可以正常使用的
-
信号函数的参数个数,少于槽函数的参数个数,此时代码无法编译通过
为什么不要求信号的参数个数和槽的参数个数严格一致,此处为什么允许信号的参数比槽的参数多呢?
因为一个槽函数,有可能会绑定多个信号
如果我们严格要求参数个数一致,就意味着信号绑定到槽的要求就变高了,换而言之
当下这样的规则,就允许信号和槽之间的绑定更灵活了,更多的信号可以绑定到这个槽函数上了
当个数不一致时,槽函数就会按照参数顺序,拿到信号的前 N 个参数
至少需要确保,槽函数的每个参数都是有值的
所以要求信号给槽的参数,可以有富裕,但是不能少
Q_OBJECT
Qt 中如果要让某个类能够使用信号槽,也就是可以在类中定义信号和槽函数,则必须要在类最开始的地方,写下 Q_OBJECT宏
这个宏能展开成很多额外的代码,Alt + 鼠标左键,点开 Q_OBJECT:
可以看到包含了这么多代码,并且这些代码还有大量的宏定义,所以还可以进一步展开,会得到一系列很复杂的代码,这些代码就涉及到 Qt 实现的内部原理了(此处就不去深入研究了)
例如在上述实现的代码中,查看 widget.h:
可以看到在最开始的地方就有 Q_OBJECT 这个宏,如果我们将这个宏注释掉,就会报错:
此时这个错误就明确说了,这个类缺少了 Q_OBJECT 这个宏
信号和槽存在的意义
所谓的信号槽,终究要解决的问题,就是响应用户的操作
而其他 GUI,例如前端页面,在响应用户操作时,每一个用户的操作都对应一个处理函数,不需要搞一个 单独的 connect 完成上述的信号槽连接
Qt 信号槽,connect 这个机制,设想很美好的:
-
解耦合. 把触发 用户操作的控件 和 处理对应用户的操作逻辑 解耦合
-
"多对多"效果 一个信号, 可以 connect 到多个槽函数上 一个槽函数,也可以被多个信号 connect
所以前端开发的机制,是完成一对一的:
一个事件只能对应一个处理函数,一个处理函数也只能对应到一个事件上
而 Qt 的机制,所期望的效果是完成多对多的,多对多这个概念在数据库中也是经常遇到的
如下所示,创建三个信号函数和三个槽函数:
此时就可以在 widget.cpp 中进行多对多的connect了:
综上所述,Qt 引入信号槽机制,最本质的目的就是:为了能够让信号和槽之间按照 "多对多" 的方式来进行关联
其他的 GUI框架往往也不具备这样的特性
实际上,随着程序开发这个事情大家经验越来越多,其实在 GUI 开发的过程中,"多对多"这件事其实是个"伪需求
实际开发很少会用到,绝大部分情况一对一就够用了
disconnect函数
使用 disconnect 来断开信号槽的连接,disconnect 使用的方式和 connect 是非常类似的
disconnect用的比较少,大部分的情况下,把信号和槽连上了之后就不必管了
主动断开往往是把信号重新绑定到另一个槽函数上
下面先创建一个按钮:
并绑定自定义的槽函数(采用connect的方式):
此时再创建第二个按钮,使用右键点击转到槽的方式,连接信号和槽
第二个按钮中会先断开 pushButton 原来的信号槽,再重新绑定信号槽
并且在两个槽函数 handleClick 和 handleClick2 中,都加入了日志信息 qDebug(),可以清楚看到发出信号后,是否调用了槽函数
运行程序:
点击第一个按钮,表示调用了 handleClick函数:
再点击第二个按钮后,点击第一个按钮,表示只调用了 handleClick2函数:
如果将这里的 disconnect 注释掉,此时 clicked 这个信号就会同时绑定两个 槽函数:
此时先点击第一个按钮:
点击完第二个按钮后,再点击第一个按钮:
观察结果可以看到,如果没有 disconnect,就会构成一个信号绑定了两个槽函数。触发信号的时候,两个槽函数都会执行
所以就证明了 Qt 的信号槽是支持多对多的,如果只是重新connect,那么新的槽函数不会去替换原来的槽函数,除非 手动 disconnect 才会解除原来绑定的关系
使用lambda表达式定义槽函数
lambda表达式,本质就是一个"匿名函数",主要应用在"回调函数"场景中,是一次性使用的
运行程序:
点击按钮:
此时就完成了功能,使用 lambda表达式,相比于前面自定义槽函数,需要声明和定义,更为简便
后续如果我们对应的槽函数比较简单,而且是一次性使用的,就经常会写作这种 lambda 的形式
lambda 语法是 C++ 11 中引入的,对于 Qt 5 及其更高版本,默认就是按照 C++ 11 来编译的
如果使用 Qt 4 或者更老的版本,就需要手动在 .pro 文件中加上 C++11 的编译选项
下面是当前版本的 .pro 文件中关于 C++11 的编译选项:
信号槽相关知识到此结束