Skip to main content
标签ad报错:该广告ID(9)不存在。
  主页 > Qt入门

Qt 信号槽与原编译系统

2024-06-04 浏览:
标签ad报错:该广告ID(7)不存在。 Qt 信号槽与原编译系统

Qt 信号槽与原编译系统

Q_Object和signal,slot的实现原理

在Qt编程中,Q_OBJECT宏、信号(signal)和槽(slot)是实现Qt对象间通信的核心机制。理解它们的实现原理对于深入掌握Qt框架非常重要。以下是对它们的分析,并结合实际的Qt5代码进行说明。

Q_OBJECT

Q_OBJECT宏通常在Qt对象的类定义中使用,它是Qt元对象系统的核心部分。它主要有两个功能:

  • 启用信号和槽机制。
  • 支持Qt的动态属性系统和运行时类型信息。

实现原理

Q_OBJECT宏在编译时会触发moc(Meta-Object Compiler,元对象编译器)的处理。moc会生成一个与类同名的C++源文件(以.moc为扩展名),其中包含该类的元数据(如信号和槽的注册信息)。

实际代码示例

// MyClass.h
#includeclass MyClass : public QObject {
    Q_OBJECT

public:
    MyClass(QObject *parent = nullptr);
    ~MyClass();

signals:
    void mySignal();

public slots:
    void mySlot();
};

在这个例子中,Q_OBJECT宏告诉moc生成MyClass的元数据。

信号和槽

信号和槽机制是Qt提供的一种对象间通信方式,信号用于发出事件通知,槽则是处理信号的函数。它们可以在不同对象间进行连接,槽函数会在信号发出时被调用。

实现原理

  • 信号(signal):在头文件中声明信号时,信号实际上被moc工具处理为一个函数,函数体为空,但其存在让Qt的元对象系统能够将其识别为信号。
  • 槽(slot):槽是普通的成员函数,但是在元对象系统中被注册为可响应信号的函数。

实际代码示例

// MyClass.cpp
#include "MyClass.h"
#includeMyClass::MyClass(QObject *parent) : QObject(parent) {}

MyClass::~MyClass() {}

void MyClass::mySlot() {
    std::cout << "Slot called!" << std::endl;
}

// main.cpp
#include#include "MyClass.h"

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    MyClass obj;
    QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot);

    emit obj.mySignal();  // This will call mySlot() and print "Slot called!"

    return app.exec();
}

运行机制

  1. moc工具:在编译时,moc工具会解析含有Q_OBJECT宏的类,并生成相应的元对象代码。该代码包含信号和槽的注册信息、动态属性和类型信息等。
  2. 运行时:Qt的QObject类及其子类会维护一个QMetaObject,其中包含类的元数据。信号发射时,通过QMetaObject找到相应的槽并调用。

QMetaObject设计

QMetaObject是Qt元对象系统的核心类,它保存了Qt对象的元数据信息,例如类名、信号、槽、属性等。QMetaObject使得Qt可以实现动态属性系统、信号与槽机制,以及类型信息的运行时访问。

QMetaObject的定义

class Q_CORE_EXPORT QMetaObject {
public:
    struct Q_CORE_EXPORT Call {};
    struct Q_CORE_EXPORT Connection {};
    struct Q_CORE_EXPORT InvokeMetaMethod {};
    struct Q_CORE_EXPORT Slot {};
    struct Q_CORE_EXPORT Constructor {};
    struct Q_CORE_EXPORT Destructor {};
    
    const char *className() const;
    const QMetaObject *superClass() const;

    int methodOffset() const;
    int methodCount() const;
    int propertyOffset() const;
    int propertyCount() const;
    int signalOffset() const;
    int signalCount() const;

    // 获取方法、属性等的元数据
    QMetaMethod method(int index) const;
    QMetaProperty property(int index) const;

    // 调用方法
    void invokeMethod(QObject *object, const char *member, Qt::ConnectionType type,
                      QGenericArgument val0 = QGenericArgument(0),
                      QGenericArgument val1 = QGenericArgument(),
                      ...);

    // 获取对象实例
    QObject *newInstance(QGenericArgument val0 = QGenericArgument(0),
                         QGenericArgument val1 = QGenericArgument(),
                         ...) const;
    
    // 查找和连接信号与槽
    int indexOfSignal(const char *signal) const;
    int indexOfSlot(const char *slot) const;

    // 静态成员
    static const QMetaObject *staticMetaObject(const char *typeName);
};

QMetaObject的组成部分

  • 基本信息:返回类名和父类的元对象。
  • 方法、属性的偏移和计数:返回方法、属性和信号的偏移和计数。
  • 元数据获取:返回指定索引的方法和属性的元数据。
  • 方法调用:通过元数据调用对象的方法,或创建对象实例。
  • 信号和槽:返回信号和槽的索引。

QMetaObject的生成与使用

QMetaObject的元数据通常由moc(Meta-Object Compiler)工具生成。moc工具解析含有Q_OBJECT宏的类,并生成相应的元对象代码。

QMetaObject在运行时的使用

在运行时,QMetaObject主要用于以下几种情况:

  • 信号和槽的连接与调用:通过QObject::connect连接信号和槽,信号发出时通过QMetaObject找到相应的槽并调用它。
  • 属性系统:可以动态访问和修改对象的属性。
  • 动态类型信息:通过QMetaObject可以获取类名、父类信息等。

信号发射函数 Q_EMIT 被定义为 空,是怎样调用到 MyClass::qt_metacall函数的

在Qt中,信号的发射和槽的调用是通过元对象系统和元对象编译器(moc)生成的代码实现的。尽管信号的定义通常为空函数,但信号的实际处理是在元对象系统中进行的。下面我们详细分析信号发射的过程以及MyClass::qt_metacall函数的调用。

信号的定义和发射

当我们在类中定义一个信号时,实际上它只是一个声明,不会有具体的实现。例如:

signals:
    void mySignal();

这个信号在编译时通过moc工具生成相应的代码。moc工具会为信号生成一个唯一的标识符,并将其注册到元对象系统中。

信号的发射过程

当我们使用emit关键字发射信号时,实际上调用的是QObject::activate函数,这个函数负责处理信号的连接和调用。

QObject::activate 的实现

static void activate(QObject *sender, int signal_index, void **argv);

这个函数的实现大致如下:

void QObject::activate(QObject *sender, int signal_index, void **argv) {
    // 查找信号的连接列表
    const QObjectPrivate *d = sender->d_func();
    const QMetaObject *meta = sender->metaObject();
    const QMetaObjectPrivate *priv = meta->d;
    QSignalSpyCallbackSet spyCallbacks = QSignalSpyCallbackSet();
    
    // 获取信号的连接
    QObjectPrivate::Connection *c = d->connections(signal_index);
    
    // 遍历所有连接的槽
    while (c) {
        // 调用槽函数
        c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, c->method_offset, argv);
        c = c->nextConnectionList;
    }
}

qt_metacall 的实现

每个包含Q_OBJECT宏的类都会由moc工具生成一个qt_metacall函数。这个函数用于调用连接到信号的槽函数。

int MyClass::qt_metacall(QMetaObject::Call call, int id, void **argv) {
    id = QObject::qt_metacall(call, id, argv);
    if (id < 0)
        return id;

    if (call == QMetaObject::InvokeMetaMethod) {
        if (id < 1) { // 假设只有一个槽
            void (MyClass::*_t)() = id ? nullptr : &MyClass::mySlot;
            if (_t) (this->*_t)();
            id -= 1;
        }
    }
    return id;
}

实际代码示例

// MyClass.h
#include#includeclass MyClass : public QObject {
    Q_OBJECT

public:
    MyClass(QObject *parent = nullptr);

signals:
    void mySignal();

public slots:
    void mySlot() {
        std::cout << "Slot called!" << std::endl;
    }
};

// MyClass.cpp
#include "MyClass.h"

// main.cpp
#include#include "MyClass.h"

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    MyClass obj;
    QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot);

    emit obj.mySignal();  // 这将调用 mySlot() 并打印 "Slot called!"

    return app.exec();
}

Qt::BlockingQueuedConnection 是怎样实现的

Qt::BlockingQueuedConnection 是 Qt 中的一种信号槽连接类型,它确保信号发射者和槽函数在不同线程时,发射信号的线程会等待槽函数执行完成。这种机制在需要同步跨线程操作时非常有用。

实现原理

  1. 信号发射线程发射信号时
    • 将信号的调用信息(包括槽函数的参数)封装成事件对象(QMetaCallEvent)。
    • 将事件对象投递到槽函数所属线程的事件队列中。
    • 等待槽函数执行完成。
  2. 槽函数所属线程处理事件时
    • 从事件队列中取出事件对象。
    • 调用槽函数。
    • 通知信号发射线程槽函数执行完成。

实际实现

信号发射线程部分

void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) {
    // ... 其他代码 ...

    // 创建 QMetaCallEvent 对象,封装信号调用信息
    QMetaCallEvent *event = new QMetaCallEvent(sender, signal_index, meta, argv);

    // 获取槽函数所属的线程
    QThread *receiverThread = c->receiver->thread();

    if (receiverThread == QThread::currentThread()) {
        // 同一线程中直接调用
        c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, c->method_offset, argv);
    } else {
        // 跨线程,使用 BlockingQueuedConnection
        QMutex mutex;
        QWaitCondition waitCondition;

        event->setWaitCondition(&mutex, &waitCondition);

        // 将事件投递到槽函数所属线程的事件队列中
        QCoreApplication::postEvent(c->receiver, event);

        // 等待槽函数执行完成
        mutex.lock();
        while (!event->isHandled()) {
            waitCondition.wait(&mutex);
        }
        mutex.unlock();
    }

    // ... 其他代码 ...
}

槽函数所属线程部分

bool QCoreApplication::notify(QObject *receiver, QEvent *event) {
    // ... 其他代码 ...

    if (event->type() == QEvent::MetaCall) {
        QMetaCallEvent *metaCallEvent = static_cast(event);

        // 调用槽函数
        receiver->qt_metacall(QMetaObject::InvokeMetaMethod, metaCallEvent->methodOffset(), metaCallEvent->args());

        // 通知发射线程槽函数执行完成
        if (metaCallEvent->isBlocking()) {
            QMutex *mutex = metaCallEvent->waitMutex();
            QWaitCondition *waitCondition = metaCallEvent->waitCondition();

            mutex->lock();
            metaCallEvent->setHandled(true);
            waitCondition->wakeAll();
            mutex->unlock();
        }
    }

    // ... 其他代码 ...
    return false;
}

QMetaCallEvent 类

class QMetaCallEvent : public QEvent {
public:
    QMetaCallEvent(QObject *sender, int signalId, const QMetaObject *meta, void **args)
        : QEvent(QEvent::MetaCall), m_sender(sender), m_signalId(signalId), m_meta(meta), m_args(args), m_handled(false), m_waitCondition(nullptr), m_mutex(nullptr) {}

    void setWaitCondition(QMutex *mutex, QWaitCondition *waitCondition) {
        m_mutex = mutex;
        m_waitCondition = waitCondition;
    }

    bool isBlocking() const { return m_waitCondition != nullptr; }
    bool isHandled() const { return m_handled; }
    void setHandled(bool handled) { m_handled = handled; }
    QWaitCondition* waitCondition() const { return m_waitCondition; }
    QMutex* waitMutex() const { return m_mutex; }

private:
    QObject *m_sender;
    int m_signalId;
    const QMetaObject *m_meta;
    void **m_args;
    bool m_handled;
    QWaitCondition *m_waitCondition;
    QMutex *m_mutex;
};

Qt::QueuedConnection 是怎样实现的

Qt::QueuedConnection 的核心思想是将信号发射事件放入接收者线程的事件队列中,等待接收者线程的事件循环处理该事件。信号发射后立即返回,不等待槽函数的执行。

实现原理

  1. 信号发射线程发射信号时
    • 将信号的调用信息(包括槽函数的参数)封装成事件对象(QMetaCallEvent)。
    • 将事件对象投递到槽函数所属线程的事件队列中。
    • 信号发射线程立即返回,不等待槽函数执行。
  2. 槽函数所属线程处理事件时
    • 从事件队列中取出事件对象。
    • 调用槽函数。

Qt::QueuedConnection 的实现

信号发射线程部分

void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) {
    // ... 其他代码 ...

    // 创建 QMetaCallEvent 对象,封装信号调用信息
    QMetaCallEvent *event = new QMetaCallEvent(sender, signal_index, meta, argv);

    // 获取槽函数所属的线程
    QThread *receiverThread = c->receiver->thread();

    if (receiverThread == QThread::currentThread()) {
        // 同一线程中直接调用
        c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, c->method_offset, argv);
    } else {
        // 跨线程,使用 QueuedConnection
        // 将事件投递到槽函数所属线程的事件队列中
        QCoreApplication::postEvent(c->receiver, event);
    }

    // ... 其他代码 ...
}

槽函数所属线程部分

bool QCoreApplication::notify(QObject *receiver, QEvent *event) {
    // ... 其他代码 ...

    if (event->type() == QEvent::MetaCall) {
        QMetaCallEvent *metaCallEvent = static_cast(event);

        // 调用槽函数
        receiver->qt_metacall(QMetaObject::InvokeMetaMethod, metaCallEvent->methodOffset(), metaCallEvent->args());
    }

    // ... 其他代码 ...
    return false;
}

QObject::connect其他两种连接类型参数是怎样实现的?和上面又有什么区别

QObject::connect 函数提供了五种连接类型:Qt::AutoConnectionQt::DirectConnectionQt::QueuedConnectionQt::BlockingQueuedConnectionQt::UniqueConnection。前面我们已经讨论了 Qt::QueuedConnectionQt::BlockingQueuedConnection。接下来我们详细分析 Qt::AutoConnectionQt::DirectConnection 的实现,以及它们与前两种连接类型的区别。

Qt::AutoConnection

Qt::AutoConnection 是默认的连接类型。它根据信号发射和槽函数是否在同一个线程中,自动选择使用 Qt::DirectConnectionQt::QueuedConnection

实现原理

  1. 同一线程:
    • 如果信号发射和槽函数在同一个线程中,使用 Qt::DirectConnection
  2. 不同线程:
    • 如果信号发射和槽函数不在同一个线程中,使用 Qt::QueuedConnection
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::AutoConnection);

代码示例

void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) {
    // ... 其他代码 ...

    QThread *senderThread = QThread::currentThread();
    QThread *receiverThread = receiver->thread();

    if (connectionType == Qt::AutoConnection) {
        if (senderThread == receiverThread) {
            // 同一线程,使用 DirectConnection
            receiver->qt_metacall(QMetaObject::InvokeMetaMethod, methodOffset, argv);
        } else {
            // 不同线程,使用 QueuedConnection
            QMetaCallEvent *event = new QMetaCallEvent(sender, signal_index, meta, argv);
            QCoreApplication::postEvent(receiver, event);
        }
    }

    // ... 其他代码 ...
}

Qt::DirectConnection

Qt::DirectConnection 直接在信号发射线程中调用槽函数。无论信号和槽是否在同一个线程中,都会同步执行槽函数。

实现原理

信号发射线程直接调用槽函数,不通过事件队列。

QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::DirectConnection);

代码示例

void QMetaObject::activate(QObject *sender, const QMetaObject *meta, int local_signal_index, void **argv) {
    // ... 其他代码 ...

    if (connectionType == Qt::DirectConnection) {
        // 直接调用槽函数
        receiver->qt_metacall(QMetaObject::InvokeMetaMethod, methodOffset, argv);
    }

    // ... 其他代码 ...
}

Qt::UniqueConnection

Qt::UniqueConnection 确保信号和槽之间只有一个连接。如果尝试创建重复的连接,会返回 false,并且不会创建重复的连接。

实现原理

检查是否已有相同的连接,如果有则不创建新的连接。

QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()), Qt::UniqueConnection);

代码示例

bool QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type) {
    // ... 其他代码 ...

    if (type & Qt::UniqueConnection) {
        // 检查是否已经有相同的连接
        if (isConnected(sender, signal, receiver, method)) {
            return false;
        }
    }

    // 根据类型创建连接
    switch (type & ~Qt::UniqueConnection) {
        case Qt::AutoConnection:
            // 创建 AutoConnection
            break;
        case Qt::DirectConnection:
            // 创建 DirectConnection
            break;
        case Qt::QueuedConnection:
            // 创建 QueuedConnection
            break;
        case Qt::BlockingQueuedConnection:
            // 创建 BlockingQueuedConnection
            break;
        default:
            // 处理默认情况
            break;
    }

    // ... 创建连接的代码 ...

    return true;
}

检查是否已有相同连接

bool QObject::isConnected(const QObject *sender, const char *signal, const QObject *receiver, const char *method) {
    // 遍历所有连接,检查是否已有相同的连接
    // 这里假设有一个连接列表 connections
    for (const auto &connection : connections) {
        if (connection.sender == sender &&
            strcmp(connection.signal, signal) == 0 &&
            connection.receiver == receiver &&
            strcmp(connection.method, method) == 0) {
            return true;
        }
    }
    return false;
}

使用示例

#include#include#includeclass MyObject : public QObject {
    Q_OBJECT

public:
    MyObject(QObject *parent = nullptr) : QObject(parent) {}

signals:
    void mySignal();

public slots:
    void mySlot1() {
        qDebug() << "Slot 1 called!";
    }

    void mySlot2() {
        qDebug() << "Slot 2 called!";
    }

    void mySlot3() {
        qDebug() << "Slot 3 called!";
    }
};

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    MyObject obj;

    // 使用 Qt::AutoConnection | Qt::UniqueConnection 连接信号和槽
    QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot1, Qt::AutoConnection | Qt::UniqueConnection);
    QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot2, Qt::AutoConnection | Qt::UniqueConnection);
    QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot3, Qt::AutoConnection | Qt::UniqueConnection);

    emit obj.mySignal();  // 调用顺序不确定

    return app.exec();
}

#include "main.moc"

为什么说,如果一个信号连接了多个槽函数,emit时,槽函数的调用顺序是不确定的?

在 Qt 中,如果一个信号连接了多个槽函数,发射信号时槽函数的调用顺序是不确定的。这是因为 Qt 的信号槽机制并不保证槽函数的调用顺序。以下是一些原因和详细解释:

1. 连接顺序不确定

在信号与槽连接时,槽函数的连接顺序不一定是按代码中出现的顺序。不同的连接操作可能在不同的时间点执行,特别是在复杂的应用中,这可能会导致连接顺序的变化。

2. 多线程环境

在多线程环境中,信号和槽可能跨线程连接。信号发射时,槽函数的调用可能被安排在不同的线程中执行,这进一步增加了调用顺序的不确定性。例如,使用 Qt::QueuedConnection 时,槽函数会被放入槽所在线程的事件队列中,事件处理的顺序依赖于线程的事件循环。

3. 内部实现

Qt 的信号槽机制内部是通过链表或其他数据结构来维护连接关系的。这些数据结构在插入和遍历时可能不会保留插入的顺序。即使在单线程环境下,内部的实现细节也可能导致槽函数的调用顺序不确定。

4. 文档说明

Qt 的官方文档明确指出,不应该依赖槽函数的调用顺序,因为这一顺序在不同的版本或不同的运行环境中可能会变化。例如:

Note: The order in which the slots are called when multiple slots are connected to a signal is undefined.

实际代码示例

#include#include#includeclass MyObject : public QObject {
    Q_OBJECT

public:
    MyObject(QObject *parent = nullptr) : QObject(parent) {}

signals:
    void mySignal();

public slots:
    void mySlot1() {
        qDebug() << "Slot 1 called!";
    }

    void mySlot2() {
        qDebug() << "Slot 2 called!";
    }

    void mySlot3() {
        qDebug() << "Slot 3 called!";
    }
};

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    MyObject obj;

    QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot1);
    QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot2);
    QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot3);

    emit obj.mySignal();  // 调用顺序不确定

    return app.exec();
}

#include "main.moc"

总结

由于连接顺序、线程环境和内部实现的影响,信号连接多个槽函数时,槽函数的调用顺序是不确定的。因此,Qt 不保证槽函数的调用顺序,并且在不同版本或环境中可能会变化。因此,依赖槽函数调用顺序的代码设计是不可靠的,应该避免。