Qt信号槽机制详解
官网原文:Signals & Slots | Qt Core 6.4.2
简介
信号槽机制是Qt的核心特性,用于对象间的通信。这可能也是与其他框架提供的特性相比最大的不同之处。信号槽机制是通过Qt的元对象系统(meta-object system)实现的。在图形界面编程时,当我们操作一个组件,我们通常希望通知到另一个组件。更通俗地说,我们希望任何类型的对象间可以互相通信。例如:如果用户单击了关闭按钮,我门可能希望窗口的close()方法被调用。
其他工具集通过回调函数实现这种通信。虽然使用这种回调函数的成熟框架的确存在,但回调函数不够直观,甚至可能遇到确认回调函数参数类型正确性的问题。
信号和槽
在Qt中,我们有一个回调函数机制的替代方案:我们使用信号槽。当一个特殊事件发生时,会发出信号。Qt的部件有很多预先定义的信号函数和槽函数,但通常的做法是继承QObject或它的子类之一(例如:QWidget),并添加自己的信号函数和槽函数。

信号槽机制是类型安全的:信号的函数签名必须与接收信号的槽函数函数签名匹配。(事实上槽函数的函数签名可能比接受的信号函数短,缺少的参数必须是信号参数的最后一个或几个参数。)因为函数签名是兼容的,当我们使用基于指针函数的语法时,编译器可以帮助我们检测类型不匹配。基于字符串的SIGNAL和SLOT语法在运行时会检测不匹配。
信号和槽是松耦合的:一个发出信号的类既不知道也不关心哪个槽接受信号。Qt的信号槽机制确保了如果你使一个信号连接到了一个槽,槽函数会在适当的时候使用信号的参数被调用。信号和槽可以接受任意数量任意类型的参数,他们是完全类型安全的。
当对象以其他对象可能感兴趣的方式改变其状态时,就会发出信号。这就是对象进行通信所做的全部工作。它既不知道也不关心是否有任何东西在接收它发出的信号。这是真正的信息封装,并确保对象可以用作软件的组件。
槽函数被用来接受信号,但他们也是普通的成员方法。就像一个对象不知道是否有任何东西接收它的信号一样,一个槽函数也不知道是否有任何信号连接到它。这确保了用Qt可以创造真正独立的组件。
你可以将任意数量的信号连接到同一个槽,也可以将一个信号连接到多个槽函数。甚至可以将一个信号连接到另一个信号。(这将在第一个信号发出时立即发出第二个信号。)
信号函数
当对象的内部状态以对象的客户端或拥有者感兴趣的方式改变时,对象就会发出信号。信号是公有访问函数,可以从任何地方发出,但我们建议只从定义信号的类或子类发出。
当一个信号发出时,连接到它的槽函数通常会立即执行,就像一般的函数调用一样。当这种情况发生时,信号槽机制完全独立于任何的GUI事件循环。一旦所有的槽函数返回,emit语句后的代码就会执行。当使用连接队列时,情况有微小不同,emit关键字后的代码会立即继续执行,槽函数会稍后执行。
如果多个槽连接到了一个信号,当信号发出时,这些槽函数会按连接的顺序一一执行。
信号函数由moc自动生成并且不能在源文件中实现。他们永远不可以有返回类型。(即:使用void)
关于参数的注意事项:我们的经验表示,如果信号槽不使用特殊的类型,他们的可重用性会更好。如果QScrollBar::valueChanged()要使用一个特殊类型,例如假设的QScrollBar::Range,那么它只能连接到专门为QScrollBar设计的插槽。将不同输入的部件连接在一块时不可能的。
槽函数
槽函数由应用程序开发者实现。
当连接到槽的信号发出时,槽会被调用。槽函数是普通的C++函数,可以以一般的方式被调用;他们唯一的特点是可以被信号函数连接。
因为槽是普通的成员函数,所以直接调用时遵循普通的c++规则。然而,作为槽函数,他们可以通过信号槽连接被任何组件调用,而忽视本身的访问级别。这意味着任意一个类的实例发出的信号可导致不相关的类的实例的私有槽函数被调用。
你也可以将槽函数定义为虚函数,我们发现这在实践中非常好用。
与回调函数函数相比,信号槽稍慢一些,因为信号槽提供了更强 的灵活性,但是这点差异对实际的应用程序来说微不足道。一般来说,发射连接到某些槽函数的信号比直接调用非虚函数调用接收器慢大约10倍。这是定位连接对象,安全地遍历所有连接(即:检查后续的接收者在发出信号期间是否被析构)以及以通用方式调用任何参数的开销。尽管10个废墟函数调用听起来很多,但是这比任何new或delete操作少很多。一旦你操作string、vector、list,背后就需要new或者delete,信号槽的开销只占整个函数的一小部分。无论何时你在槽函数中进行系统调用也是如此;或者直接调用十个以上的函数。信号槽的简单性和灵活性是值得这个开销的,甚至你的有洪湖不会注意到。
注意,当与基于Qt的应用程序一起编译时,可能导致其他定义了signals或slots变量的库发出警告或报错。为了解决这个问题,#undef违规的预处理器符号。
例程
所有包含信号槽的类必须在类定义的顶部声名Q_OBJECT这个宏,并且该类必须直接或间接继承Qobjects。
宏Q_OBJECT会被编译器展开为一些成员方法的声名,这些方法会被moc实现。如果你收到编译器的报错“undefined reference to vtable for LcdNumber”,你可能忘了运行moc或者在link命令中包含moc的输出。
请注意,只有value_不等于new_value时,setValue()函数才会改变value_的值并发出信号。这防止环形连接情况下的无限循环。
默认情况下,每个建立的连接都会有一个信号发出;重复连接会发出两个信号。你可以调用disconnect()中断所有的连接。如果你传递了Qt::UniqueConnection类型,只有当连接不是副本时才会被建立。如果已经有个副本(对于同样一对对象,相同的信号函数已经连接到了相同的槽函数),这个连接会返回失败。
真实案例
#ifndef LCDNUMBER_H #define LCDNUMBER_H #include <QFrame> #include <QObject> class LcdNumber : public QFrame { Q_OBJECT public: LcdNumber(QWidget * parent = nullptr); signals: void overflow(); // The LcdNumber class emits a signal, overflow(), when it is asked to show an impossible value. public slots: void display(int num); void display(double num); void display(const std::string & str); void setHexMode(); void setDecMode(); void setOctMode(); void setBinMode(); void setSmallDecimalPoint(bool point); }; #endif // LCDNUMBER_H
请注意,display()重载了;当你将信号函数连接到槽函数时,Qt将选择适当的版本。对于回调函数,你必须使用五个不同的函数名称并自己跟踪类型。
带默认参数的信号槽
信号函数和槽函数的签名可能包含参数,这些参数可以有默认值。考虑Qobject::destroyed():
void destroyed(QObject* = nullptr);
当QObject被delete时,它会发出Qobject::destroyed()信号。我们想要捕获这个信号,无论在哪里,我们可能有一个对已delete的QObject的悬空引用,所以我们可以清理它。合适的槽函数签名可能是:
void objectDestroyed(QObject* obj = nullptr);
连接信号槽有几种方法
我们使用Qobject::connect()
第一种,使用函数指针
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
将Qobject::connect()与函数指针一起使用有几个好处。首先,它允许编译器检查信号函数的函数和槽函数的参数是否匹配。如果需要,参数能被编译器隐式转换。
第二种,你也可以连接到仿函数或c++11的lambda表达式
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });
在这两种情况下,我们都在connect()调用中提供了上下文。context对象提供了接收者应该在哪个线程中执行的信息。这很重要,因为提供上下文可以确保接收者在上下文线程中执行。
当发送方或上下文被销毁时,lambda将被断开连接。您应该注意,当信号发出时,函子内使用的任何对象仍然是活动的。
第三种,使用QObject::connect()和signal和slot宏。关于是否在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()重载时,编译器不会检查信号和槽参数。
信号槽高级使用方法
QObject *QObject::sender() const
lambda表达式很方便用于将自定义参数传给槽函数
connect(action, &QAction::triggered, engine,[=]() { engine->processAction(action->text()); });
共同使用Qt和第三方信号槽
Qt可以与第三方信号/插槽机制一起使用。您甚至可以在同一个项目中使用这两种机制。要做到这一点,将以下内容写入您的CMake项目文件:
target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)
在qmake项目(.pro)文件中,你需要写:
CONFIG += no_keywords
它告诉Qt不要定义moc关键字signals、slots和emit,因为这些名称将被第三方库使用,例如Boost。然后,要继续使用带有no_keywords标志的Qt信号和插槽,只需将源代码中Qt moc关键字的所有使用替换为相应的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT。
基于Qt的库中的信号槽
基于qt的库的公共API应该使用关键字Q_SIGNALS和Q_SLOTS,而不是signals和slots。否则,很难在定义了QT_NO_KEYWORDS的项目中使用这样的库。
为了实施这个限制,库创建者可以在构建库时设置预处理器定义QT_NO_SIGNALS_SLOTS_KEYWORDS。
这个定义排除了信号和槽,而不影响是否可以在库实现中使用其他特定于qt的关键字。
                    
                
                
            
        
浙公网安备 33010602011771号