一文详解Qt信号和槽函数机制

描述

一、信号和槽函数机制简介

(注1:下文中的槽函数表示一个意思)

(注2:阅读本文可能有点枯燥,但文中有关于信号和槽的重要知识,这些知识甚至在开发中经常被忽略。请君继续下看)

信号和槽用于多个对象之间的通信。信号和槽机制是Qt的核心特性,也是Qt与其他框架最大的不同之处。Qt的元对象系统是信号和槽实现的基础。

在GUI编程中,当更改一个小部件时,通常希望另一个小部件得到通知。希望任何类型的对象之间都能够相互通信。例如,如果用户单击关闭按钮,可能希望调用窗口的Close()函数。

其他软件工具包或框架可能使用回调机制实现这种通信机制。一个回调函数是一个指向一个函数的指针,所以如果想让一个处理函数通知一些事件,可以向处理函数传递一个指向另一个函数(回调函数)的指针,然后处理函数在适当的时候调用回调函数。虽然使用这种方法的成功框架确实存在,但回调可能不太直观,在确保回调参数类型的正确性上可能会存在问题。

在Qt中,有一种回调技术的替代方法:那就是信号和槽机制。当特定事件发生时,会发出一个信号。Qt的小部件中有许多预定义的信号,但我们可以将小部件子类化,向它们添加自定义的信号。槽是响应特定信号的函数。Qt的小部件有许多预定义的槽函数,但是通常是子类化小部件并添加自己的槽函数,这样就可以处理与之相关联的信号了。如下图所示:

GUI

信号和槽机制是类型安全的:信号的参数必须与槽函数的参数相匹配。(实际上,槽的参数可以比它接收到的信号参数更少,因为槽可以忽略额外的参数)由于参数是兼容的,所以在使用基于函数指针语法的信号与槽关联机制时,编译器可以帮助检测类型是否匹配,从而可以检测出在开发中信号和槽函数关联时出现的问题。

信号和槽函数是松耦合的:当一个对象发出信号,该对象不知道也不关心哪个对象的槽函数会接收这个信号。Qt的信号和槽函数机制确保:如果将一个信号连接到一个槽函数上,该槽函数将在正确的时间被调用。信号和槽函数可以接受任意数量的任意类型的参数。它们完全是类型安全的。所有从QObject或它的一个子类(例如,QWidget)继承的类都可以使用信号和槽槽函数机制。当对象改变其状态时,可能就会发出信号(这一点由开发人员和父类确定其关联的信号什么时候发出)。

槽函数用来接收信号,但也是普通的成员函数。就像对象不知道是否有东西接收到它的信号一样,槽函数也不知道是否有信号连接到它,因此可以创建独立的软件组件。当需要使用该独立组件时,确定其组件类中预定义的信号和槽函数,然后关联信号和槽函数即可。

可以将多个信号连接到一个槽函数上(即【多对一】),而一个信号也可以连接到多个槽函数上【即一对多】。

也可以将一个信号直接连接到另一个信号。(当第一个信号发出时,它将立即发出第二个信号。)

综上,在Qt中,信号和槽函数共同构成了一个功能强大的组件编程机制。

二、信号

(2-1)信号的发出

由于某种条件到达可能引起了对象改变,其内部状态将发生改变,这时候对象就会发出信号。信号是公共访问函数,可以从任何地方发出,但是建议:【只从定义该信号的类及其子类发出信号】

在Qt框架下,信号发出分为两种:

1、【每个类预定义的信号】:这些信号何时发出可以通过查看官方文档获知。

2、【自定义的信号】:这些信号的发出由开发人员自行定义。

(2-2)信号的处理

当一个信号发出时,连接到它的槽函数通常会立即执行,就像一个普通函数调用一样。在这种情况下,信号和槽函数机制是完全独立于GUI事件循环的,也并不会干扰GUI的事件循环。emit语句之后的代码将在所有槽函数都返回之后才执行。如果使用排队连接(queued connections),情况略有不同,在这种情况下,emit关键字后面的代码将立即继续,槽函数将在后续执行。

如果几个槽函数连接到同一个信号上,当信号发出时,这些槽函数将按照它们连接时的顺序依次执行【这一点很重要】。

信号是由moc工具自动生成,不能在.cpp文件中实现,所以信号永远不能有返回类型(必须使用void关键字定义信号)。

关于信号和槽参数的注意事项:经验表明,如果信号和槽函数不使用特殊类型,那么代码具有极强的可重用性。

下表是使用connect()创建信号和槽函数连接时,可以指定5种不同的连接类型:

序号 类型 含义
1 Qt::AutoConnection 如果接收者生活在发出信号的线程中,Qt::DirectConnection被使用。否则,使用Qt::QueuedConnection。连接类型是在信号发出时确定。【这是Qt创建信号和槽函数时的默认连接方式】
2 Qt::DirectConnection 当信号发出时,槽函数立即被调用。槽函数在发送信号的线程中执行。
3 Qt::QueuedConnection 当控制返回到接收方线程的事件循环时,将调用槽函数。槽函数在接收方的线程中执行。
4 Qt::BlockingQueuedConnection 与Qt::QueuedConnection相同,只是在槽函数返回之前线程会阻塞。如果接收方存在于发送信号的线程中,则不能使用此连接,否则应用程序将会死锁。
5 Qt::UniqueConnection 这是一个标志,可以使用按位OR与上述的连接类型进行组合。当Qt::UniqueConnection被设置时,如果连接已经存在,QObject::connect()将失败(例如,如果相同的信号已经连接到同一对对象的相同槽位)。注:这个标志在Qt 4.6中引入。

三、槽函数

当一个连接到槽函数的信号被发射时,槽函数将被调用。槽函数是普通的C++函数,在实际开发中也可以正常调用;它们唯一的特点是:【信号可以与它们相连接】。

由于槽是普通的成员函数,所以它们在直接调用时遵循普通的C++规则。但是,作为槽函数时,任何组件都可以通过信号连接从而调用它们。

还可以将槽函数定义为虚拟的,这在开发中非常有用。

与回调机制相比,信号和槽函数机制的速度稍微慢一些,这一点对于实际应用程序来说,这种差别并不显著。一般来说,发送一个连接到某些槽函数的信号,比直接调用非虚函数要慢大约10倍。这是定位连接对象、安全地遍历所有连接(即检查后续接收方在发射过程中没有被销毁)以及以函数调用增加的开销。虽然10个非虚函数调用听起来很多,但是它比new操作或delete操作的开销要小得多。一旦在后台执行一个需要new或delete的字符串、向量或列表操作,信号和槽函数的开销只占整个函数调用开销的很小一部分。在槽函数中执行系统调用时也是如此(或间接调用超过十个函数)。因此信号和槽函数机制的简单性和灵活性是值得的,这些开销在实际应用场景下甚至不会注意到。

注意,当与基于Qt的应用程序一起编译时,定义为信号或槽的变量的第三方库可能会导致编译器出现警告和错误。要解决这个问题,使用#undef来定义出错的预处理器符号即可。

(3-1)带有默认参数的信号和槽函数

信号和槽可以包含参数,参数可以有默认值。例如:QObject::destroyed():

 

void destroyed(QObject* = nullptr);

 

当QObject被删除时,它会发出这个QObject::destroyed()信号。无论我们在哪里有一个悬空引用指向已删除的QObject,都希望捕捉到这个信号,这样就可以清除它。合适的槽参数可以是:

 

void objectDestroyed(QObject* obj = nullptr);

 

(3-2)使用QObject::connect()将信号连接到槽函数的三种方法

1、第一种方法:使用函数指针

 

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

 

将QObject::connect()与函数指针一起使用有几个优点。它允许编译器检查信号的参数是否与槽的参数兼容。当然,编译器还可以隐式地转换参数。

2、第二种方法:连接到C++ 11的lambdas

 

connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });

 

在这种情况下,我们在connect()调用中提供这个上下文。上下文对象提供关于应该在哪个线程中执行接收器的信息。

当发送方或上下文被销毁时,lambda将断开连接。注意:当信号发出时,函数内部使用的所有对象依然是激活的。

3、第三种方法:使用QObject::connect()以及信号和槽声明宏。

在SIGNAL()和SLOT()宏中包含参数(如果参数有默认值)的规则是:传递给SIGNAL()宏的参数不能少于传递给SLOT()宏的参数

例如以下代码都是合法的:

 

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

 

但是这种是非法的:

 

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

 

因为槽函数期望的是一个信号不会发送的QObject。此连接将报告运行时错误。

注意,使用这种方法时,在使用QObject::connect()关联信号和槽函数时,编译器不会自动检查信号和槽函数的参数之间是否匹配。

综上:使用第一种方法 创建信号和槽 在开发中较为常用,也较为合适。

(3-3)信号和槽函数的一些高级用法

当需要获取信号发送方的信息时,使用Qt提供QObject::sender()函数,该函数返回一个指向发送信号对象的指针。

Lambda表达式是传递自定义参数到槽的一种方便方式:

 

connect(action, &QAction::triggered, engine,[=]() { engine->processAction(action->text()); });

 

四、使用disconnect断开信号/槽连接

disconnect()用于断开对象发送器中的信号与对象接收器中的方法的连接。如果连接成功断开,则返回true;否则返回false。

当对信号/槽关联的两方中的任何一个对象进行销毁时,信号/槽连接将被移除。

disconnect()有三种使用方法,如下示例所示:

1、断开所有与对象相连的信号/槽:

 

disconnect(myObject, nullptr, nullptr, nullptr);

 

相当于非静态重载函数:

 

myObject->disconnect();

 

2、断开所有与特定信号相连的对象:

 

disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr);

 

相当于非静态重载函数:

 

myObject->disconnect(SIGNAL(mySignal()));

 

3、断开特定接收对象的连接:

 

disconnect(myObject, nullptr, myReceiver, nullptr);

 

相当于非静态重载函数:

 

myObject->disconnect(myReceiver);

 

nullptr可以用作通配符,分别表示“任何信号”、“任何接收对象”或“接收对象中的任何槽”。

如下格式的使用示例:

 

disconnect(发送对象,信号,接收对象,方法)

 

发送对象不会是nullptr。

如果信号为nullptr,将断开接收对象槽函数与所有信号的连接。否则只断开指定的信号

如果接收对象是nullptr,它断开所有关联该信号的连接。否则,只断开与接收对象的槽函数连接。

如果方法是nullptr,它会断开任何连接到接收对象的连接。如果不是,只有命名为方法的槽函数连接将被断开。如果没有接收对象方法必须为nullptr。即:

 

disconnect(发送对象,信号,nullptr,nullptr)

 

五、使用Qt与第三方信号和槽函数

当第三方库中也有信号/槽函数机制时,这时候又需要使用Qt的信号和槽函数机制。对于这种开发场景,Qt可以在同一个项目中使用这两种机制。需将下面一行添加到qmake项目(.pro)工程配置文件中:

 

CONFIG += no_keywords

 

该配置将告诉Qt不要定义moc关键字信号、槽函数和emit,因为这些名称将被第三方库使用(例如Boost)。如果要在使用no_keywords标志下继续使用Qt信号和槽机制,需将源文件中所有的Qt moc关键字替换为对应的Qt宏:Q_SIGNALS(或Q_SIGNAL)、Q_SLOT(或Q_SLOT)和Q_EMIT。

六、总结

本文站在开发的角度,描述了Qt的信号和槽函数机制。

  审核编辑:汤梓红
 
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分