Window系统下开发Qt——信号与槽原理
Qt的核心之一就是信号与槽,信号与槽实现采用了类似观察者模式。
在面向对象的编程中,都会创建很多实例,而每个实例都是独立的,要想每个实例能够协同合作,那么就会需要一种对象间传递消息的机制,在很多框架中都采用回调函数来进行对象间信息传递。
回调函数就是一个函数指针,如果想要一个处理函数通知一些事件,你需要将这个指针传递给处理函数,处理函数在适当时间调用回调函数。MFC就是使用的回调函数,但回调可能是不直观的,不易于理解的,并且也不能保证是类型安全的。
一、 回调函数
所谓的对象之间的通信,从程序设计语言语法角度来看就是函数调用的问题,只不过是某个对象的成员函数调用另一个对象的成员函数而已,本文从语法角度讲解信号和槽的原理,这样更容易理解信号和槽的实现原理。
(1)、函数调用的几种形式
如上图,假设函数f需要g的处理结果,有以下几种处理方式:
最简单的方式就是直接调用函数g,但这种方式有一个明显的缺点,必须知道函数g的名称“g”以及函数g的参数类型。但是若f只需要g的处理结果就可以了,而g的处理结果不一定是由函数g来完成,它也可以是x、y或其他函数来完成,那么这种直接调用函数的方式就无法胜任了,因为系统不知道用户会使用哪个函数来完成这种处理结果,也就是系统不知道调用的函数名究竟是g、x或其他名称。
另一种方式就是回调函数,即在函数f中使用一个指向函数的指针去调用需要的函数,这样就可以调用任意名称的函数(只要函数类型与指针相同即可),此时只要是完成了函数g功能的函数都可以作为函数f的结果被调用,这样就不会被函数名称所限制。比如
(2)信号与槽 与 回调函数的区别
回调函数的本质是基于“你想让别人的代码执行你的代码,而别人的代码你又不能动”这种情况产生的。回调函数是函数指针的一种用法,如果多个类都需要关注某个类的状态变化,此时需要在被被关注的类中维护一个列表,以存放多个回调函数的地址。对于每一个被关注的类,都需要做类似的工作,因此这种做法不灵活。回调函数的例子:
【领更多QT学习资料,点击下方链接免费领取↓↓,先码住不迷路~】
点击→Qt开发(资料笔记文档+视频教程+项目实战)
Qt为了消除回调函数等的弊端,从而开发了一种新的消息传递机制,即信号和槽。
顺带提一句,Qt提供了一种机制,能够自动、有效的组织和管理继承自QObject的Qt对象,这种机制就是对象树。这种机制在界面变成上是有好处的,能够帮助程序员缓解内存泄露的问题,比如当应用程序创建了一个具有父窗口部件的对象时,该对象将被加入父窗口部件的孩子列表。当应用程序销毁父窗口部件时,该父对象的孩子列表中的对象将被一一删除。这让我们在编程时,能够将主要精力放在系统的业务上,提高编程效率,同时也保证了系统的稳健性。所以new了一个父窗口后,只要delete父窗口后,那它的子窗口都会被自动释放,释放顺序(即析构顺序)与这些子对象的构造顺序相反。
例如,当我们要求鼠标点击某个按钮时,对应的窗口就需要关闭,那么这个按钮就会发出一个点击信号,而窗口接收到这个信号后执行关闭窗口。那么,这个信号就是按钮被点击,而槽就是窗口执行关闭函数。可以将信号和槽理解成“命令-执行”,即信号就是命令,槽就是执行命令。
二、信号与槽的一般用法
(1)信号
当一个对象的内部状态发生改变时,如果其它对象对它的状态改变需要作出反应,这时就应该让这个类发出状态改变的信号。声明信号使用SIGNALs关键字。发送信号使用emit关键字。信号的使用需要主要的点:
所有的信号声明都是公有的,所以Qt规定不能在signals前面加public、private、protected;
所有的信号都没有返回值,所以返回值都用void;
所有的信号都不需要定义,只需要声明就可以;
信号所属的类必须直接或间接继承自QOBject类,并且开头包含Q_OBJECT。
在同一个线程中,当一个信号被emit发出时,会立即执行其槽函数,等槽函数执行完毕后,才会执行emit后面的代码,如果一个信号连接了多个槽,那么会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照它们连接时的顺序执行的。不同线程中(即跨线程时),槽函数的执行顺序是随机的。
信号与槽机制要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,信号的参数可以比槽函数的参数多,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不允许槽函数的参数比信号的多,因为信号根本没有这个数据,也就无法在槽函数中使用。
(2)槽
槽其实就是普通的C++函数,它可以是虚函数,static函数,也可以被重载,可以是公有的、保护的、私有的,当然也可以被其他c++成员函数调用,它唯一特点就是能和信号连接。当和它连接的信号被发出时,这个槽就会被调用,而且槽所在的类也需要直接或间接继承QObject,然后添加Q_OBJECT,槽函数因为是普通的C++函数,所以需要实现。
在Qt4中,声明槽可以使用:public/protected/private slots:
在Qt5中不需要使用这些声明,每个函数都可以被当作是槽函数,而且还可以使用Lambda表达式来作为槽。不过为了程序的可读性,还是推荐槽函数要声明一下。
(3)信号与槽的连接
使用connect函数,有两个原型。
原型1:
如:
Qt4和Qt5都可以使用这种连接方式。
原型2:
如:
connect(pushButton, &QPushButton::clicked, dialog, &QDialog::close);
这是Qt5新增的连接方式,这使得在编译期间就可以进行拼写检查,参数检查,类型检查,并且支持相容参数的兼容性转换。
(4)信号与槽的多种用法
一个信号可以和多个槽相连
这时槽的执行顺序和在不在同一个线程上有关,同一线程,槽的执行顺序和声明顺序有关,跨线程时,执行顺序是不确定的。
多个信号可以连接到一个槽
只要任意一个信号发出,这个槽就会被调用。
一个信号可以连接到另外的一个信号
当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。
槽可以被取消连接
主动取消连接使用disconnect()函数。
使用Lambda 表达式
能够支持 Qt 5 的编译器都是支持 Lambda 表达式。
(5)connect的第5个参数
第5个参数可以取以下的值,分别代表的意义如下:
(6)相关函数怎么获取信号发送者
当多个信号连接一个槽时,有时需要判断是哪个对象发来的,那么可以调用sender()函数获取对象指针,返回为QObject指针。
二、信号与槽实现原理
现在来看看信号与槽怎么到底怎么实现的? 这里为了说明信号与槽的原理,下面以一个简单的信号与槽例程来说明。代码如下:
定义一个类SignalsAndSlots,在类中定义一个信号和槽。
SignalsAndSlots.h文件
SignalsAndSlots.cpp 文件
【领更多QT学习资料,点击下方链接免费领取↓↓,先码住不迷路~】
点击→Qt开发(资料笔记文档+视频教程+项目实战)
main.cpp 文件
编译程序,会出现以下编译错误。
(1)moc预编译器
moc(Meta-Object Compiler)元对象预编译器。moc读取c++头文件。如果它找到包含Q_OBJECT宏的一个或多个类声明,它会生成一个包含这些类的元对象代码的c++源文件,并且以moc_作为前缀。信号和槽机制、运行时类型信息和动态属性系统需要元对象代码。由moc生成的c++源文件必须编译并与类的实现联系起来。通常,moc不是手工调用的,而是由构建系统自动调用的,因此它不需要程序员额外的工作。
(2)Q_OBJECT 宏
下面看看Q_OBJECT真面目,其宏定义如下:
将其中的宏再次展开,并去掉一下无用的代码,如下:
你也可以在signalsandslots.h中用上面的代码替换掉Q_OBJECT ,但你会发现还需要实现Q_OBJECT扩展后所带来的变量和函数的定义。而这些定义都已经被写入到了moc_signalsandslots.cpp文件中了,这也就是为什么需要将moc_signalsandslots.cpp一起编译的原因了。否则,这个类是不完整的。
打开生成的moc_signalsandslots.cpp文件,看看里面代码。你需要在moc_signalsandslots.cpp文件从下往上看代码:
/*1.首先初始化静态变量staticMetaObject,并为QMetaObject中的无名结构体赋值*/
/*2.执行对象所对应的信号或槽,或查找槽索引*/
/*3.存储元对象信息,包括信号和槽机制、运行时类型信息和动态属性系统*/
/*4.初始化qt_meta_stringdata_SignalsAndSlots,并且将所有函数拼接成字符串
【领更多QT学习资料,点击下方链接免费领取↓↓,先码住不迷路~】
点击→Qt开发(资料笔记文档+视频教程+项目实战)
/*5.切分字符串*/
/*6.存储类中的函数及参数信息*/
从上面的代码中,我们得知Qt的元对象系统:信号槽,属性系统,运行时类信息都存储在静态对象staticMetaObject中。
接下来是对另外三个公有接口的定义,在你的代码中也可以直接调用下面的函数:
//1、获取元对象,可以调用
获取类名称
//2、这个函数负责将传递来到的类字符串描述,转化为void*
//3、调用方法
接下来,我们发现在头文件中声明的信号,其真正定义是在这里,这也是为什么signal不需要我们定义的原因。
运行结果
(3)关键字
说是关键字,其实不准确,实际是宏。
signals
如果signals被展开的话就是public,所以所有的信号都是公有的,也不需要像槽一样加public,protected,private的限定符。
slots
slots和signals一样,只是没有了限定符,所以它是否可以被对象调用,就看需求了。
emit
它的宏定义:
# define emit
emit是个空的宏。当它被替换的时候,相当于没有任何作用。程序其实就是调用了sigPrint()函数,而不是真正意义上的发送一个信号,有很多初学者都是认为当emit的时候,Qt会发信号,实际就是普通函数调用。
(4)信号与槽的实际流程
通过以上的代码和一顿操作,我们来总结一下信号与槽的具体流程。
moc编译器(Qt提供)查找头文件中的signals,slots,标记出信号和槽。
将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
通过active函数找到在map中找到所有与信号对应的槽索引
根据槽索引找到槽函数,执行槽函数。
以上,便是信号槽的整个流程,总的来说就是一个“注册-索引”机制,并不存在发送系统信号之类的事情。
(5)信号与槽的注意点
1、槽的属性
public slots:在这个区内声明的槽意味着所有对象都可将信号和之相连接。这对于组件编程非常有用,你能创建彼此互不了解的对象,将他们的信号和槽进行连接以便信息能够正确的传递。
protected slots:在这个区内声明的槽意味着当前类及其子类能将信号和之相连接。这适用于那些槽,他们是类实现的一部分,不过其界面接口却面向外部。
private slots:在这个区内声明的槽意味着只有类自己能将信号和之相连接。这适用于联系非常紧密的类。
2、如果发射者和接收者属于同一个对象的话,那么在connect调用中接收者参数能省略。
3、有三种情况可使用disconnect()函数:
(1)断开和某个对象相关联的所有对象。事实上,当我们在某个对象中定义了一个或多个信号,这些信号和另外若干个对象中的槽相关联,如果我们要切断这些关联的话,就能利用这个方法,非常之简洁。
(2)断开和某个特定信号的所有关联。
disconnect( myObject, SIGNAL(mySignal()), 0, 0 ) 或 myObject->disconnect( SIGNAL(mySignal()) )
(3)断开两个对象之间的关联。
disconnect( myObject, 0, myReceiver, 0 ) 或 myObject->disconnect( myReceiver )
在disconnect函数中0能用作一个通配符,分别表示所有信号、所有接收对象、接收对象中的所有槽函数。不过发射者sender不能为0,其他三个参数的值能等于0。
4、定义不能用在signal和slot的参数中。
因为moc工具不扩展#define,所以在signals和slots中携带参数的宏就不能正确地工作。
#define SIGNEDNESS(a) unsigned a
signals:
void someSignal( SIGNEDNESS(a) );
5、构造函数不能用在signals或slots声明区域内。
6、函数指针不能直接作为信号或槽的参数,是不合法的,可以取巧,用typedef,如下:
typedef void (*ApplyFunctionType)(QList*, void*);
public slots:
void apply( ApplyFunctionType, char *);
7、信号和槽不能有缺省参数:因为signal与slot绑定是发生在运行时。
8、信号和槽也不能携带模板类参数
如果将信号、槽声明为模板类参数的话,即使moc工具不报告错误,也不可能得到预期的结果,也可以取巧,用typedef
typedef pair IntPair;
public slots:
void setLocation (IntPair location);
9、嵌套的类不能位于信号或槽区域内,也不能有信号或槽。即类b嵌套在类a内,想在类b中声明信号与槽是不行的。
10、友元声明不能位于信号或槽声明区内。相反,他们应该在普通C++的private、protected或public区内进行声明
(6)槽的执行时间大于信号发送间隔怎么办?
有两种情况:
1、如果需要对每个发来的信号都做出处理,那么有两种方式来解决,即在信号与槽的connect函数中明确第五个参数,将其设置成DirectConnection方式阻塞时编程,或者设置成BlockingQueuedConnection阻塞的方式都可以很好的解决;
2、如果只需要对最新的信号做处理,那么这里也给出两种方案来处理:
a、槽所在线程设置bool状态,信号所在线程通过判定这个bool的状态来确定是否发送信号;
b、槽执行完毕,则向信号所在线程发送返回值,信号所在线程通过判定发来的这个返回值来判定是否继续对槽所在线程发送新的信号。