"流云开一朵~"
如果你系统地学习过Linux,你一定对"信号"一词不会感到陌生。当我们打开命令行终端,在键盘上敲击"Ctrl +C"、"Ctrl+Z"等组合键,当前命令行执行的程序就会收到系统发出的信号。信号的三要素:信号源、信号类型、信号处理方式,其在Qt中也有信号的概念,它与操作系统层面上的信号有何区别呢?
------前言
信号和槽概述
在 Qt 中,⽤⼾和控件的每次交互过程称为⼀个事件。"用户关闭窗口"是一个实践,"用户"点击按钮是一个事件。每个事件都会发出⼀个信号,例如⽤⼾点击按钮会发出 "按钮被点击" 的号,用户关闭窗口时会发出"窗口被关闭"的信号。
信号发出只是一个过程,或者是只是一种前提。当我们看到红绿灯时,灯光发出红光是一种信号,但我们更要关心的是,这种信号对应要做出的行为是,"停在白线以内,等待绿灯亮起"。显然,这种行为是约定俗成的规定。
Qt中的所有控件都具有接收信号的能力,⼀个控件还可以接收多个不同的信号。对于接收到的每一个信号,都会做出相应的响应动作。而在 Qt 中,对信号做出的响应动作就称之为槽。
🥚 因此,Qt中的信号槽机制,就是给用户发起的事件,添加处理函数。
信号与槽关联函数
当你为一个信号,量身定做了一个函数。可是,你却不让这个信号知道,自己有一个DIY。就像你不告诉小孩子,"在绿灯的条件下,路过斑马线",而顽皮的孩童只知道"过斑马线"。
Connect函数
cpp
QMetaObject::Connection QObject::connect(const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method, Qt::ConnectionType type = Qt::AutoConnection)
# Sender: 信号发出者
# signal: 信号类型
# recviver: 信号接收者
# method: 槽函数(信号处理方法)
注:在Linux中也有一个connect函数,只不过它是在网络编程TCP之中。两者不是同一类
槽的本质
槽(Slot)就是对信号响应的函数。槽就是⼀个函数,与⼀般的 C++ 函数是⼀样的,可以定义在类的任何位置。可以具有任何参数,可以被重载,也可以被直接调⽤。
槽函数与⼀般的函数不同的是:
槽函数可以与⼀个信号关联,当信号发射时,关联的槽函数会⾃动执⾏。
槽函数与信号函数
🍮 信号和槽机制底层是通过函数间的相互调⽤实现的:
每个信号都可以⽤函数来表⽰,称为信号函数;每个槽也可以⽤函数表⽰,称为槽函数。
🍮 信号函数和槽函数通常位于某个类中:
信号函数⽤ signals 关键字修饰,槽函数⽤public slots、protected slots或者private slots修饰。
信号函数只需要声明,不需要定义(实现),⽽槽函数需要定义(实现)。
信号与槽的使用
建立信号与槽的联系,我们可以使用两种方式:
使用代码建立
cpp
connect(myButton,&QPushButton::clicked,this,&Widget::handler_button);
slots:
void Widget::handler_button()
{
if(myButton->text() == "hello qt"){
myButton->setText("hello world");
}
else{
myButton->setText("hello qt");
}
}
当我们运行程序后,点击该按钮时,就会发出信号。
接收信号,执行我们connect好的槽函数,完成整个信号处理:
使用ui建立
选择"转到槽":
选择"clicked()"函数,这是QPushButto组件里自带的信号函数。
点击确定后,会在Widget文件下,创建一个新的函数------"槽函数"。
点击按钮后,文本框里的内容发生改变:
我们在编写Qt代码时,使用、控制控件的方式有两种,一种是代码的方式,另一种则是使用ui。ui方式不过是使用xml格式,最终还是会转为C++文件,等待编译。
至于选择哪一种,就看谁方便,就用哪一种。
内置信号与槽
我们通过代码能够直观地使用信号与槽了,可是,我作为一个初学者,咋知道要选择Clicked()作为信号函数呢?这个问题,我们得打开Qt 为我们使用者提供的官方文档:
去Contents中寻找"signals",如果每找到,就去它继承类查找:
进入QAbstractButton后,我们就能够在Content中找到Signals了,其中包含了如下内置的信号函数、槽函数。
在Qt中如果想要让某一个类能够使用信号槽,要在类最开始的地方写下:"Q_OBJECT"
自动生成槽函数:
在通过ui方式建立信号与槽函数的连接时,我们发现"转到槽"生效后,会自动生成:
函数。
⾃动⽣成槽函数的名称有⼀定的规则。槽函数的命名规则为:on_XXX_SSS,其中:
🍧 以 " on " 开头,中间使⽤下划线连接起来;
🍧 " XXX " 表⽰的是对象名(控件的 objectName 属性);
🍧 " SSS " 表⽰的是对应的信号。
正如:" on_pushButton_clicked() " ,pushButton 代表的是对象名,clicked 是对应的信号。
按照这种命名⻛格定义的槽函数, 就会被 Qt ⾃动的和对应的信号进⾏连接。但一般情况下,除非是IDE自动生成。我们手动进行connect更好,⼀⽅⾯显式 connect 可以更清晰直观的描述信号和槽的连接关系;另⼀⽅⾯也防⽌信号或者槽的名字拼写错误导致连接失效。
再谈connect函数:
在前面,我们谈及了connect函数是什么,以及它的用法:
我们也知道函数传参是这个过程:
但char* 是一个指针类型,函数指针又是另外一种指针类型,两者压根不是同类型的,怎么可能进行赋值呢?
我们使用Qt 帮助手册中查到,要使用这个connect函数,进行函数传递的时候,需要搭配SINGAL、SLOT宏,这些宏才能够让我们传递的函数指针转变为char*。但,在之前我们压根没有使用SLOT、SIGNAL宏完成这一步必要的转换啊!
从Qt 5开始,上述写法做出了简化,不必再使用SLOT、SINGAL宏了。connect提供了重载版本,在这个重载版本中,第二个和第四个参数成为了泛型参数,允许传入任意指针类型了:
这里泛型参数使用了封装的类型萃取器,能够在编译时得到传入参数的类型,当然这不是本篇要细说的。
此时的connect函数还具备了一定的 "参数检查功能",一旦传入的第一个第二个、第三个第四个形参类型不匹配,就会编译报错。
自定义信号与槽
在 Qt 中,允许⾃定义信号的发送⽅以及接收⽅,即可以⾃定义信号函数和槽函数。但是对于⾃定义的信号函数和槽函数有⼀定的书写规范。
⾃定义信号函数书写规范:
🍿⾃定义信号函数必须写到 "signals" 下.
🍿返回值必须为void,只需要声明,不需要实现.
🍿可以有参数,也可以发⽣重载.
作为信号是一种非常特殊的函数,程序员只需要写出函数声明,告诉Qt这是一个信号即可。这个函数的定义,会在Qt的编译过程中自动生成。
⾃定义槽函数书写规范:
🍜 早期的 Qt 版本要求槽函数必须写到 "public slots" 下但是现在⾼级版本的 Qt 允许写到类"public"作用域或者全局下.
🍜 返回值为 void,需要声明,也需要实现.
🍜 可以有参数,可以发⽣重载.
使⽤ "emit" 关键字发送信号。 "emit" 是⼀个空的宏。"emit" 其实是可选的,没有什么含义,只 是为了提醒开发⼈员。同样,直接替代使用函数名也是可以的,但尽量选择前者。
建立自定义信号与槽:
在 widget.h 中声明⾃定义的信号和槽,如图所⽰:
在 widget.cpp 中实现槽函数,并且关联信号和槽:
带参数的信号与槽:
Qt 的信号和槽也⽀持带有参数, 同时也可以⽀持重载。此处我们要求:
🍓 信号函数的参数列表要和对应连接的槽函数参数列表⼀致,通过这样的机制, 就可以让信号给槽传递数据。
重载信号槽和信号函数:
多对多
信号函数与槽函数的连接方式有哪几种呢?
所以,一个结论:一个槽函数可以被多个信号绑定,一个信号可以绑定多个槽函数。
在实际写代码的过程中,我们也会发现信号函数与槽函数参数是有特点的:
这是因为,一个槽函数可以被多个信号绑定。如果严格要求,信号函数的参数与槽函数参数一致,在这样的规则下,绑定会变得不够灵活。
当与信号参数不一致时,至少能够拿到前N个参数,保证槽函数中的每个参数都是有值的。
所以,一个结论:信号函数的参数个数多于或等于槽函数的参数个数。
如何理解多对多?
把槽函数这个词放到系统编程中,通常被称为:"回调函数"。当事件机制被触发,就会执行回调函数。这种一对一的模式下,一个处理函数只能对应到一个事件上。
Qt中一个信号可以选择多个槽函数,一个槽函数可以与多个信号关联,只要你不手动断开双方的连接,一旦信号发射,就会执行槽函数。此时,这个connect就如同一个关联表一样:记录着信号与槽函数关联的细节信息。
Qt引入信号槽机制,本心是为了让"信号与槽"按照多对多的关系进行关联,从而能够应付复杂的现实问题。但, 在GUI图形化界面的开发过程中,压根不需要这种"复杂的需求","一对一"模式是仅够了的。
信号槽补充
(1) 断开信号与槽
使⽤ disconnect 即可完成断开. disconnect 的⽤法和 connect 基本一致。
(2) Lambda表达式
Qt5 在 Qt4 的基础上提⾼了信号与槽的灵活性,允许使⽤任意函数作为槽函数。 但如果想⽅便的编写槽函数,⽐如在编写函数时连函数名都不想定义,则可以通过 Lambda表达式来达到这个⽬的。
注:Lambda表达式不是本篇需要细讲的
当我们不断发射信号,就会执行connect中绑定的Lambda表达式。不过,Lambda表达式是C++11引入的,所以在负责编译的配置文件中,要加上:(默认是加了的)
信号与槽的优缺点
优点: 松散耦合
🍁信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了⾃ ⼰,Qt的信号槽机制保证了信号与槽函数的调⽤。
🍁⽀持信号槽机制的类或者⽗类必须继承于 QObject 类。
缺点: 效率较低
🍁 与回调函数相⽐,信号和槽慢⼀些,因为它们提供更⾼的灵活性。但在实际应用中差别 不⼤。
总结:
① 什么是信号?什么是槽?
② Qt内置的信号与槽函数
③ 自定义信号与槽函数:不带参的、带参的
④ disconnect、Lambda用法简化槽函数定义
本篇到此结束,感谢你的阅读。
祝你好运,向阳而生~