目录
前言
一、初始信号与槽
1、信号与槽的本质
2、信号与槽的使用
3、内置信号、内置槽函数与自定义信号、自定义槽函数
(1)文档查询
(2)自定义信号与内置槽函数的使用
4、信号与槽函数关联关系
5、带参数的信号与槽函数
6、信号与槽函数的重载
二、深剖信号与槽函数细节
1、宏函数 SIGNAL 与 SLOT
2、为啥通过 ui 指针可以操作界面中拖动创建的控件
前言
本文主要学习QT最重要的机制之一 —— 信号与槽;从认识信号与槽到熟练掌握、深刻理解信号与槽相关细节;
一、初始信号与槽
不少朋友在听到信号这一词可能会想到 Linux 中的信号机制,本文中的信号与 Linux 中的信号没有任何关系,但是两者之间又非常的相似;当某一时间到来时,操作系统给当前进程发送一个信号,当进程收到这个信号后会调用指定的处理方法来处理这个信号,关于信号更多细节可以关注下面这篇文章;
Linux | 信号-CSDN博客
1、信号与槽的本质
信号本质:在 Qt 中,所谓信号就是某个事件,当某个控件发生了某个事件时,可以根据这个事件做出指定动作;这里的事件就是信号,比如 Qt 中的按钮QPushButton,这个控件被点击,这个点击就是一个事件;
槽本质:在 Qt 中,所谓槽的本质就是收到信号时,我们对应需要进行处理这个信号的动作,还是以上述按钮为例,我们希望用户点击按钮时,窗口的标题会发生改变,这里窗口标题发生改变就是槽函数所处理的动作;
2、信号与槽的使用
我们通过 connect 来将某个信号与槽函数来进行绑定,从而达到某个信号发出时,指定的槽函数会被调用;connect 函数如下所示;
QMetaObject::Connection QObject::connect
(
const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection
)
这样拆解不知道看起来是否更加清晰,该函数有五个参数,其中第五个参数一般为缺省即可,因此我们仅仅需要关注前四个参数;
参数一:该参数类型为QObject类的指针,为信号的发出者的地址;
参数二:该参数为 const char* ,该参数为对象发出的信号;
参数三:该参数类型也为QObject类指针,为信号的接收者,也是处理信号的对象;
参数四:该参数为 const char* ,为接收者所要处理的动作;
注意:
1、第一个参数和第三个参数为QObject类,该类为 Qt 提供的一个基类,Qt 为我们提供的所有类都继承自该类,该类可以说是 Qt 所有内置类的 "祖先类";联想一下C++中切片,也就是说派生类可以赋值给基类;
2、参数二和参数四为const char*类型,而实际上,我们传入的信号和槽函数都是一个函数指针,因此在使用时,我们需要使用SIGNAL和SLOTS将对应的函数指针转换成const char*类型;
需求设计:我们在ui页面中拖出一个按钮组件,我们期望按下这个按钮指定的窗口也关闭;
1、我们先创建一个基于Widget类的窗口项目;
2、我们点击widget.ui文件,绘制一个按钮,并设置按钮文本
3、信号与槽函数关联
注意到这一步时,我们有两种方法,两种方法各有优劣,实际开发中,哪种方法方便用哪种即可;
3.1、代码添加连接
1)切换到 widget.h 文件,写下槽函数声明
注意这里的函数声明中,有一个我们陌生的关键字,slots,这个是 Qt 自己设置的关键字,同样,我们还可以与 protected、private组合;这个关键字表示声明的是槽函数;在 Qt5 后提出可以省略该关键字;
2)编写槽函数
这里教大家一个小技巧,我们可以直接选中我们函数声明,按住键盘 alt + enter,可以直接一键生成函数定义;生成的函数定义当然放在widget.cc文件,声明与定义分离,这一点以后就不在重复;
这里再次补充一下,qDebug是 Qt 为我们提供的打印类,我们使用这个来替代cout;
3)建立连接
注意:这里我们第二个参数和第四个参数并没有使用宏函数SIGNAL()与SLOTS(),这一点我们后面说明;
参数一:这里我们用ui界面绘制的这个按钮,因此我们在ui这个指针下寻找按钮对象;
参数二:这里clicked表示点击信号,这里点击表示鼠标按下并抬起;同样还有如下信号;
关于这里的状态切换以及点击带参数这两个可能大家不大理解,这里暂不做介绍,后面讲解复选框自然会介绍;
参数三:处理信号的对象,也就是接收者;
参数四:信号的处理动作;(这里动作也就是槽函数,我们可以使用 Qt 为我们提供的,也可以使用自己写的,这里用自己写的,填写我们刚写下的槽函数地址即可)
此时,我们想要的功能便实现了,可以点击运行项目,点击按钮后会关闭窗口,并且还会打印内容;接下来介绍第二种方法,我们重新创建一个新的项目;
3.2、自动添加链接
1)还是在ui界面绘制一个按钮,步骤就不再赘述,与上述方法中步骤一相同
2)右击按钮,选择转到槽
此时会弹出如下窗口,实际上,这里就是选择按钮发出的信号,等同于我们调用connect时前两个参数的填写;这里前五个信号,我们在上述都讲过的,这里不再赘述;我们直接选择clicked;
此时会在widget.cpp文件自动生成函数定义,且这个函数定义名字是有讲究的,我们不能修改这个槽函数的名字,否则会关联失败,具体原因,后面会详细介绍;
3)编写槽函数
到这一步,我们连接就完毕了,所有工作完成;我们可以直接运行程序查看效果;
3、内置信号、内置槽函数与自定义信号、自定义槽函数
上述案例中,我们使用了内置信号clicked,与自定义槽函数handler,实际上, Qt 也为我们提供了一些内置信号与槽函数,我们可以通过Qt Creator的帮助文档中查询;下面我们以 QpushButton 为例;
(1)文档查询
点击 Qt Creator 左侧帮助一栏;
在索引中输入指定目标,这里输入 QPushButton;
该类相关接口菜单如下(右边那个,用红色框圈住了菜单中常用的);
这里我们发现,我们并没有看到信号,实际上由于继承这一特性,信号很可能在父类,我们可以通过如下方式找到其父类;
我们点击找到其父类,我们在父类的属性菜单中,很容易找到了信号与槽函数字段;
如下所示,这信号不就是我们刚刚讲过的信号吗?我们便可以通过这样的方式进行文档查询;
(2)自定义信号与内置槽函数的使用
之前那个关闭窗口,我们是使用内置信号 clicked 与自定义槽函数 handler 来实现的;下面我们再使用 自定义信号与内置槽函数 close 再来实现一次;我们首先再次创建一个新项目;
还是一样,ui界面绘制按钮,如下所示;
widget.h文件声明信号;
注意:信号仅需声明,无需定义,且无返回值;
我们在使用connect关联函数;
这就完了吗?并没有,这里我们仅仅只是进行了信号关联,我们并没有发送这个信号;我们可以使用 emit关键字来发送信号;我们再返回ui页面,右击按钮,转到槽,选择clicked;然后我们再这个槽函数发送信号;
编译运行程序,此时我们便可以点击按钮,发出自定义信号,接着窗口就会便关闭;
4、信号与槽函数关联关系
一对一:一个信号对应一个槽函数;
一对多:一个信号对应多个槽函数;
多对多:多个信号对应多个槽函数;
之前,我们都是采用一对一的方式,接下来,我们再来试试一对多;我们再创建一个空项目,继承自widget类;
绘制出一个按钮,如下所示;
在widget声明一个信号与两个槽函数;
实现这两个槽函数,槽函数啥都不干,完成一个打印自己函数名的工作即可;
为这两个槽函数建立连接,都连接到 mySignal 这一个信号上;
我们想点击按钮,执行这两个槽函数,因此我们为按钮的点击信号设置一个槽函数;来到设计师界面(ui界面),右击按钮,转到槽,选择clicked信号;并编写点击按钮对应槽函数;
此时我们编译运行程序,我们每次点击按钮都会发送要给 mySignal 信号,而这个信号与 handler1 与 handler2 槽函数都进行了关联,这两个槽函数都会被执行,因此一次点击就会有两个打印结果;
同样多对多的方式我就不一一演示了;有兴趣的可以自己下去做实验;
拓展:我们不仅可以信号与槽函数进行关联,我们还以信号与信号关联,一个信号被触发,他会触发另一个信号;
5、带参数的信号与槽函数
我们的信号与槽函数可以带参,通过参数,将信号的参数传递给槽函数;不过我们必须遵守如下规则;
1、参数类型匹配
2、信号的参数个数要大于等于槽函数参数
我们声明一个信号,两个槽函数,如下所示;
接下来,我们在.cpp文件中对这两个槽函数进行定义;
我们在构造函数中连接并发送这个信号;
接着运行程序,如下所示;
6、信号与槽函数的重载
信号与槽函数可以进行重载,只不过在重载后,进行connect时,需要显示指定connect哪一个重载函数;
依然,我们创建一个信号,用这个信号连接两个重载槽函数;首先创建新项目,声明如下信号与槽函数;
我们在对声明槽函数进行定义,如下所示;
最后对槽函数进行连接,主要是这里连接的代码,由于函数重载,我们如果直接连接,不知道连接到哪一个槽函数,因此我们需要先显示指定出类型,然后再连接;如下代码所示;
尤其是红色框内的代码逻辑,函数指针那块代码可能会有些绕;
二、深剖信号与槽函数细节
1、宏函数 SIGNAL 与 SLOT
前面我们说过connect第二个和第四个参数是 const char* 类型,需要用宏 SIGNAL 与 SLOT 将函数指针转换成 const char* 类型;可我们上述所有代码都没有使用这两个宏函数;这是因为在 Qt5 中,提供了这个函数的另一个版本;
template <typename PointerToMemberFunction>
QMetaObject::Connection QObject::connect
(
const QObject *sender,
PointerToMemberFunction signal,
const QObject *receiver,PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection
)
这个PointerToMemberFunction是一个模板,这里涉及C++中萃取技术;因此在 Qt5 中,我们通常不用使用这两个宏函数;
2、为啥通过 ui 指针可以操作界面中拖动创建的控件
首先我们来看 ui 指针到底是什么类型,这个指针声明在 widget.h 文件中;
它的类型是Ui命名空间里的Widget类指针;注意,我们这个 ui 指针所在类的类名也是Widget,而这个Widget并不在Ui这个命名空间内;那么Ui命名空间的Widget在哪里声明的呢?
实际上,Qt 采用元编程的技术,所谓元编程,就是用代码生成代码,我们的 Qt 代码首先经过 qmake 编译器,编译出C++代码,再拿这个C++代码生成最后可执行程序;
如何证明?
我们编译程序后,会在同级目录下生成一个Build文件夹,这个文件夹就会存放qmake编译生成的代码;
以HelloWorld项目为例;我们点金对应生成的Build文件夹;
这个ui_widget.h 文件便是我们 设计师页面,生成的头文件,我们点进这个文件;
Ui命名空间内的Widget便是继承于Ui_Widget类;
我们通过拖动控件的方式来构建一个Label标签;如下所示;
我们重新编译,接着来看Build文件夹下的 Ui_Widget.h 文件;
这时,我们的 Ui_Widget.h 文件多了一个QLable控件;同样的代码,我们给这个QLabel 改个名字;我们再观察这个文件;我们把这个QLabel控件的名字改为 label_1234;
我们观察 Ui_Widget.h 文件,发现QLabel标签的名字果然发生了改变;
综上所述,我们通过ui指针本质就是 Ui_Widget 类型的指针,而这个类是 widget.ui 文件经过qmake编译后,生成了一个Ui_Widget.h 的文件,而这个文件内有 Ui_Widget 这个类;
而我们 Widget.h 文件中声明的 ui 指针正是Ui命名空间中的 Widget 类,而该类中又有我们通过 Widget.ui 文件中拖动生成的各类控件;故我们可以通过 ui 指针来操作界面中拖动创建的控件;