信号与槽是QT的核心机制,它提供了一种强大的对象间通信方式,用来替代传统C++中脆弱、紧耦合的回调函数(函数指针、std::function等)。
信号与槽是什么?能解决什么问题?
基本概念:
-
信号: 当对象的状态发生改变或某个事件发生时,它所发生的一个“通知”。信号是一个函数声明,由moc自动实现,开发者只需要在类中声明。信号函数不需要实现,也不能被调用,只能使用emit关键字发射。
-
槽:是一个普通的C++成员函数,他可以像普通函数一样被调用,但可以被一个信号“触发”调用。槽函数负责对与其连接的信号做出响应,执行具体的逻辑。
解决的问题: 在传统的C++中,对象A想要通知对象B,通常需要持有B的指针或引用,并直接调用B的某个方法。这导致了紧耦合:A必须知道B的具体类型和接口。如果C也想被通知,A的代码就需要修改。
信号与槽通过“间接调用”实现了“松耦合”:
- 发送者:(如一个按钮) 不知道也不关心谁接收它的通知。
- 接收者(如一个窗口) 只需要将自己的一个槽函数连接到感兴趣的信号上。
- Qt框架作为“中介”,维护着信号与槽的连接关系,并在信号发射时自动调用与之连接的槽函数。
使用connect函数建立连接
使用QObject::connect 函数建立信号与槽的连接是Qt编程中最常见的操作。Qt提供了两种主流的语法:字符串语法和函数指针语法。 ** 函数指针语法**(Qt5及以上)
这是现代Qt代码中的首选连接方式, 它在编译时进行类型检查,更安全,并且支持C++11的lambda表达式。
使用Lambda表达式:
对于简单的响应逻辑,无需专门定义槽函数,可以使用lambda表达式,这使得代码非常紧凑。
自定义信号与槽,以及emit关键字
要使用自定义信号与槽,您的类必须直接或间接继承自QObject,并在类定义的私有部分使用 Q_OBJECT 宏。
自定义槽
槽就是普通的成员函数,可以在public slots、protected slots或private slots区域声明(对于函数指针语法,slots关键字不是必须的,但显式声明有助于阅读)。
自定义信号
信号在类的signals区域声明。信号没有返回值(必须是void类型),但可以有任意数量和类型的参数。信号只需声明,不需实现
发射信号
使用 emit 关键字来发射信号。emit是一个空的宏,它不会做任何操作,仅从语法上提示这里正在发射一个信号。
参数传递:信号的参数会原样传递给与之连接的槽函数。因此,连接的信号和槽的参数类型和数量必须兼容。槽函数的参数数量可以少于信号的参数数量,但类型和顺序必须匹配,多余的参数会被忽略。
连接类型:自动连接与队列连接
connect 函数的最后一个可选参数用于指定连接类型,它决定了槽函数在何种线程上下文中被调用。这对于多线程编程至关重要。
- Qt::AutoConnection (默认)
○ 行为:如果信号发射者与槽接收对象在同一个线程,则使用Qt::DirectConnection;如果它们在不同线程,则使用Qt::QueuedConnection。
○ 这是最常用且最安全的方式。
- Qt::DirectConnection
○ 行为:当信号发射后,立即在发射信号的线程中直接调用槽函数。这相当于一个同步函数调用。
○ 使用场景:性能要求极高,且确保线程安全的场景。如果跨线程使用,槽函数会在发送者线程执行,通常很危险。
- Qt::QueuedConnection
○ 行为:当信号发射后,槽函数不会立即被调用。信号被封装成一个事件,放入接收对象所在线程的事件队列中。当接收者线程处理到该事件时,才会在自己的线程上下文中调用槽函数。
○ 使用场景:跨线程通信的标准方式。它保证了槽函数在它所属的线程中被安全调用,是GUI线程与工作线程通信的基石。
- Qt::BlockingQueuedConnection
○ 行为:类似QueuedConnection,但是信号发射者线程会阻塞,直到槽函数在接收者线程中执行完毕。
○ 使用场景:需要等待另一个线程处理结果的场景。必须谨慎使用,否则极易造成死锁。
本章总结
信号与槽是Qt的命脉。您需要掌握:
-
概念:理解其作为松耦合通信机制的本质。
-
语法:熟练使用函数指针语法进行连接,并了解Lambda表达式的应用。
-
自定义:能够在自定义类中声明和发射信号、定义槽函数。
-
连接类型:深刻理解不同连接类型的含义,特别是QueuedConnection在跨线程通信中的关键作用。