Qt的信号槽机制

1. 什么是元对象编译器和元对象系统?

在开始讲信号槽之前,我们先了解下Qt的框架的核心组成部分,Qt的元对象编译器(MOC)和元对象系统是Qt框架的核心组成部分,它们使得Qt拥有了信号与槽机制、反射(introspection)和属性系统等强大的特性。下面分别解释这两个概念:

1.1 元对象编译器(MOC)

元对象编译器是Qt特有的一个预处理器,它不是标准C++的一部分。MOC会处理使用了Qt的特殊宏(如`Q_OBJECT`、`signals`、`slots`等)的C++头文件。它的主要任务是为使用了这些宏的类生成额外的C++源代码文件。这个生成的源文件包含了元信息以及信号和槽机制所需的实现代码。

MOC生成的代码包括但不限于:

  • 类的元信息(类名、父类、信号和槽的名称等)。

  • 用于实现信号和槽机制的函数,包括信号发射的存根和用于槽调用的代码。

  • 用于动态属性系统的代码。

  • 实现Qt的反射机制的代码,允许在运行时查询对象的类型信息和成员函数。

1.2 元对象系统

元对象系统是Qt运行时环境的一部分,它使用MOC生成的代码来提供动态特性,如:

  • 信号与槽:一个高级的事件订阅和通知机制,用于对象间的通信。

  • 对象反射:允许在运行时查询对象的类型信息(如它的类名、它继承的基类、它拥有的信号和槽)。

  • 属性系统:允许在运行时查询和修改对象的属性。

  • 动态方法调用:允许在运行时调用对象的方法。

每个通过`Q_OBJECT`宏声明的类,都能在运行时通过其元对象(`QMetaObject`)来访问这些特性。`QMetaObject`实例包含了关于其对应类的所有信息,包括信号、槽、属性等。这种机制使得Qt的对象可以在运行时进行更多动态操作。

简而言之,元对象编译器为元对象系统生成必需的胶水代码,而元对象系统则利用这些代码,在运行时提供动态的、反射式的特性。这两个元素共同构成了Qt框架中对象间通信和动态类型管理的基础。

2. 信号槽机制

2.1 元对象编译器(MOC):

MOC扫描通过`Q_OBJECT`宏标记的类,并为这些类生成附加的C++代码。这个代码包括信号和槽的定义,以及类的元信息(如类名、信号/槽列表、属性等)。 假设有如下类定义:

cpp 复制代码
 class MyClass : public QObject {
       Q_OBJECT

   public:
       MyClass(QObject *parent = nullptr) : QObject(parent)   signals:
       void mySignal(int);

   public slots:
       void mySlot(int);
   };

MOC将为此类生成一个名为`moc_myclass.cpp`的源文件,其中包含了用于信号和槽的实现细节。这个文件通常包括:

  • 信号的存根(stub)函数

  • 类的元信息(元对象代码)

  • 用于调用槽的静态函数

2.1.1 信号的存根(stub)函数是什么?

在Qt的信号和槽机制中,信号的存根(stub)函数是MOC生成的一段代码,它充当信号的实现。在Qt中,信号函数本身是不包含用户定义的实现的;你只需要在类的头文件中声明它们。当你在代码中发射(emit)一个信号时,实际上是调用了这个存根函数。

存根函数的主要职责是通知Qt元对象系统有信号发生,并传递任何相关的参数。然后,元对象系统负责调用和这个信号相关联的所有槽函数。

这里是一个信号存根函数的简化示例(伪代码):

cpp 复制代码
// MyClass 类中的信号声明部分
class MyClass : public QObject {
    Q_OBJECT

public:
    ...

signals:
    void mySignal(int value);

    ...
};

// MOC 生成的存根函数
void MyClass::mySignal(int value) {
    // 内部生成的代码,用来激活信号
    QMetaObject::activate(this, &MyClass::staticMetaObject, signalIndex, &value);
}

在上面的代码中,`mySignal` 信号的存根函数被MOC生成,并且包含了调用 `QMetaObject::activate` 函数的代码。这个 `activate` 函数是元对象系统的一部分,它负责查找所有连接到 `mySignal` 信号的槽,并依次调用它们。

请注意,开发者不需要编写信号的存根函数;它们是由MOC自动根据类的头文件生成的。开发者只需要声明信号,并在必要的时候使用 `emit` 关键字来发射它们。例如:

cpp 复制代码
emit mySignal(123);

这行代码在运行时实际上调用的就是MOC为 `mySignal` 信号生成的存根函数。

2.1.2 activate 函数运行原理

在Qt中,信号和槽之间的连接是通过 `QObject::connect` 函数建立的。这个函数告诉Qt元对象系统,当特定的信号被发射时,应该调用哪个槽函数。连接可以在运行时动态建立,也就是说,在程序的执行过程中,可以根据需要将任何信号连接到任何槽上。

下面是一个信号和槽连接的例子:

cpp 复制代码
QObject::connect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName);

在这个例子中,我们假设 `sender` 是一个指向 `SenderClass` 实例的指针,而 `SenderClass` 中有一个名为 `signalName` 的信号。`receiver` 是指向 `ReceiverClass` 实例的指针,`ReceiverClass` 中有一个名为 `slotName` 的槽函数。

当 `connect` 函数被调用时,Qt元对象系统会记录下信号和槽之间的连接。这个信息被用于在信号发射时,查找和调用所有连接到该信号的槽函数。

`QMetaObject::activate` 函数内部的工作原理如下:

  1. 当信号发射时(即,当存根函数被调用时),`activate` 函数被执行。

  2. `activate` 函数查询内部的连接列表,这个列表记录了所有连接到该信号的槽。

  3. 对于每一个连接,`activate` 函数会调用相应的槽函数。如果槽函数接受参数,`activate` 会传递信号的参数给槽。

这个过程是由Qt的元对象系统在背后自动管理的,开发者不需要编写代码来处理这些低级细节。您只需要知道如何使用 `connect` 函数来建立连接,以及如何使用 `emit` 关键字来发射信号。

Qt的信号和槽机制非常强大,因为它允许对象之间进行松耦合的通信。槽函数不需要知道是哪个信号触发了它们,也不需要知道信号来自哪个对象。同样,对象可以发射信号而不需要知道谁将接收它们。这种机制极大地提高了代码的可重用性和可维护性。

2.1.3 activate函数怎么知道哪个信号发送了?

`activate`函数是Qt元对象系统的一部分,用于在运行时处理信号的发射和槽的调用。它知道是哪个信号被发射的,因为每次信号发射时,存根函数都会传递特定的信息给`activate`函数。

这个过程中涉及到几个关键的步骤和元素:

1. 信号发射(Emission):

  • 当你调用一个信号(如`emit mySignal(value);`),实际上你调用的是由MOC为该信号生成的存根函数。

2. 信号索引(Signal Index):

  • 每个信号在其类的元对象中都有一个唯一的索引值。这个索引是在编译时由MOC根据信号在类中的声明顺序计算得出的。

  • 当存根函数被调用时,它使用这个索引作为参数之一调用`QMetaObject::activate`函数。

3. 参数传递(Parameter Passing):

  • 除了信号索引,存根函数还将信号的参数作为参数传递给`activate`函数。这样,`activate`函数就可以将这些参数传递给目标槽函数。

4. 槽函数调用(Slot Invocation):

  • `QMetaObject::activate`函数使用信号索引来查找所有连接到该信号的槽,并使用信号的参数来调用它们。连接信息存储在内部的数据结构中,这些数据结构在运行时通过`QObject::connect`函数填充。

下面是`QMetaObject::activate`函数调用的伪代码流程:

cpp 复制代码
// 假设这是由MOC生成的存根函数的调用
void MyClass::mySignal(int value) {
    // ...省略其他代码...
    QMetaObject::activate(this, &MyClass::staticMetaObject, signalIndex, &value);
}

// activate函数的概念实现
void QMetaObject::activate(QObject *sender, QMetaObject *m, int local_signal_index, void **argv) {
    // 确定全局信号索引
    int signal_index = m->methodOffset() + local_signal_index;

    // 查找对应于信号的所有连接,并调用相应的槽
    foreach (const Connection &c, connectionsForSignal(signal_index)) {
        if (c.receiver) {
            c.slot_method(sender, argv); // argv 包含了所有传递给信号的参数
        }
    }
}

在这个示例中,`activate`函数接收到了信号索引和信号参数。这个索引用于在信号和槽的连接表中查找应该被调用的槽函数。然后,`activate`函数根据这些信息调用所有连接的槽函数,并将信号参数传递给它们。

这样,`activate`函数就可以知道是哪个信号被发射,并且能够将该信号路由到所有已连接的槽函数。这是Qt信号和槽机制的核心,允许对象之间进行灵活和动态的通信。

2. 2 信号和槽的存储:

MOC生成的元信息包含信号和槽的名称和参数类型。这个信息存储在每个对象实例的元对象中。

2.3. 信号和槽的连接:

连接信号和槽时使用的`QObject::connect()`函数大致上是这样的:

cpp 复制代码
QObject::connect(&sender, &MyClass::mySignal, &receiver, &MyClass::mySlot);

在内部,`QObject::connect()`会创建一个连接数据结构,它包含了关于信号和槽的信息,以及它们所属的对象。

2.4. 信号的发射:

当你调用`emit mySignal(10);`时,MOC为`mySignal`生成的代码会被执行。这段代码会遍历所有与该信号连接的槽,并调用它们。这通常是通过调用`QMetaObject::activate()`来完成的。

cpp 复制代码
  // 伪代码
   void MyClass::mySignal(int value) {
       // MOC生成的信号函数
       QMetaObject::activate(this, &MyClass::staticMetaObject, signalIndex, &value);
   }

2.5. 槽函数的调用:

槽函数的调用是通过`QMetaObject::activate()`在运行时通过元对象系统完成的。当一个信号被激活时,Qt会查找与之相连接的所有槽,并逐个调用它们。

cpp 复制代码
   // 伪代码
   void QMetaObject::activate(QObject *sender, QMetaObject *m, int local_signal_index, void **argv) {
       // 遍历连接列表
       for (每个连接到该信号的接收器) {
           // 调用槽函数
           slot = connection->slot;
           (receiver->*slot)(/* 参数转换和传递 */);
       }
   }

如果信号和槽在不同的线程,Qt会安排一个事件(QEvent)并将其发送到接收者所在的线程的事件队列中,事件处理函数将在目标线程中调用槽函数。

信号和槽机制的核心是QObject和QMetaObject。QObject提供了基础的通信能力,而QMetaObject负责存储类的元信息和提供动态类型检查和方法调用等能力。通过MOC生成的代码和这些类的合作,Qt可以在运行时动态地连接对象,传递参数,并且安全地调用方法。这就是Qt信号和槽机制强大灵活性的来源。

3. connect的第五个参数

在Qt中,`QObject::connect` 方法有一个重载版本,它接受第五个参数,这个参数是一个枚举 `Qt::ConnectionType`,它指定了信号和槽之间连接的类型。`Qt::ConnectionType` 枚举的值决定了信号是直接发送到槽,还是通过事件队列来进行异步调用。这个参数是可选的,如果不提供,默认是 `Qt::AutoConnection`。

`Qt::ConnectionType` 枚举的几个可能的值包括:

  • `Qt::AutoConnection` (默认): Qt会根据接收者是否位于发射者的线程中自动选择是使用 `Qt::DirectConnection` 还是 `Qt::QueuedConnection`。如果接收者和发射者在同一个线程,它会使用 `Qt::DirectConnection`,否则会使用 `Qt::QueuedConnection`。

  • `Qt::DirectConnection`: 槽函数会在信号发射的那一刻立即被调用,无论接收者和发射者是否在同一个线程。这意味着槽函数是在信号发射者的线程上下文中执行的。

-`Qt::QueuedConnection`: 发射信号的事件会被放入接收者所在的线程的事件队列中。接收者的槽函数将会在接收者所在的线程的事件循环中稍后被调用。这种连接类型在跨线程通信时特别有用。

  • `Qt::BlockingQueuedConnection`: 类似于 `Qt::QueuedConnection`,但是发射信号的线程会等到接收者线程中的槽函数执行完毕后再继续执行。这种类型的连接必须在不同线程之间使用,否则会导致死锁。

  • **`Qt::UniqueConnection`:**这个参数可以和其他类型组合使用(通过按位或操作)。它确保不会为同一信号和槽创建重复的连接。如果尝试重复连接,`connect` 函数将不会建立连接并返回 `false`。

下面是一个带有第五个参数的 `connect` 函数的例子:

cpp 复制代码
QObject::connect(sender, &SenderClass::signalName,
 receiver, &ReceiverClass::slotName, Qt::QueuedConnection);

在这个例子中,当 `signalName` 被发射时,`slotName` 槽函数将通过事件队列异步调用。这是跨线程工作时的一个典型用例。

相关推荐
Bruce小鬼4 分钟前
QT文件基本操作
开发语言·qt
2202_7544215410 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热16 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
懷淰メ26 分钟前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
hummhumm40 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
宁静@星空1 小时前
006-自定义枚举注解
java·开发语言
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
武子康1 小时前
Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据库·sql·mybatis·springboot
珹洺1 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
每天吃饭的羊1 小时前
python里的数据结构
开发语言·python