1. 程式人生 > >深入分析Qt訊號與槽(下)

深入分析Qt訊號與槽(下)

今天我們終於可以看看神奇的訊號槽是怎麼實現的了。話不多說,直接上程式碼。

示例程式

新建控制檯應用程式,再新增一個新類SignalsAndSlots3,各自定義一個訊號和槽,程式碼如下:
signalsandslots3.h:

class SignalsAndSlots3 : public QObject
{
    Q_OBJECT
public:
    explicit SignalsAndSlots3(QObject *parent = 0);

signals:
    void sigPrint(const QString& text);

public slots:
    void
sltPrint(const QString& text); };

signalsandslots3.cpp:

#include <QDebug>
#include "signalsandslots3.h"

SignalsAndSlots3::SignalsAndSlots3(QObject *parent) : QObject(parent)
{
    connect(this, SIGNAL(sigPrint(QString)), this, SLOT(sltPrint(QString)));
    emit sigPrint("Hello");
}

void
SignalsAndSlots3::sltPrint(const QString &text) { qDebug() << text; }

main.cpp:

#include <QCoreApplication>
#include "signalsandslots3.h"

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

    SignalsAndSlots3 s;

    return a.exec();
}

本節為了說明原理,所以只寫了最簡單的訊號槽。

編譯執行程式,在控制檯會輸出Hello字樣。

Makefile檔案

現在我們開啟Qt自動生成的Makefile.Debug檔案。找到下面這一行:

SOURCES = F:/Study/Qt/Projects/QtShareCode/chapter2/2-3/SignalsAndSlots3/main.cpp
F:/Study/Qt/Projects/QtShareCode/chapter2/2-3/SignalsAndSlots3/signalsandslots3.cpp debug/moc_signalsandslots3.cpp

  關鍵看加粗的部分,我們看到signalsandslots3.cpp和moc_signalsandslots3.cpp作為原始檔一起進行編譯。

由此可知,Qt又額外生成了moc_signalsandslots3.cpp檔案,其名稱為,同名原始檔前加上了moc字首。

moc預編譯器

  moc(Meta-Object Compiler)元物件預編譯器。

  moc讀取一個c++標頭檔案。如果它找到包含Q_OBJECT巨集的一個或多個類宣告,它會生成一個包含這些類的元物件程式碼的c++原始檔,並且以moc_作為字首。

  訊號和槽機制、執行時型別資訊和動態屬性系統需要元物件程式碼。由moc生成的c++原始檔必須編譯並與類的實現聯絡起來。通常,moc不是手工呼叫的,而是由構建系統自動呼叫的,因此它不需要程式設計師額外的工作。

Q_OBJECT巨集

#define Q_OBJECT \
public: \
    Q_OBJECT_CHECK \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

  我們都知道巨集會在預編譯期被具體的字串所代替,那麼我們在標頭檔案中用到的Q_OBJECT巨集就會被展開為上面的程式碼。

  你可以在signalsandslots3.h中用上面的程式碼替換掉Q_OBJECT ,你會發現還需要實現Q_OBJECT擴充套件後所帶來的變數和函式的定義。而這些定義都已經被寫入到了moc_signalsandslots3.cpp檔案中了,這也就是為什麼在Makefile中需要將moc_signalsandslots3.cpp一起編譯的原因了。否則,這個類是不完整的,那肯定也是不可能編譯通過的。

moc_signalsandslots3.cpp

從標頭檔案中得出,我們首先需要定義

static const QMetaObject staticMetaObject;

你需要從下往上看程式碼

/*
6.儲存類中的函式及引數資訊
*/
struct qt_meta_stringdata_SignalsAndSlots3_t {
    QByteArrayData data[5];//函式加引數共5個
    char stringdata0[41];//總字串長41
};

/*
5.切分字串
*/
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_SignalsAndSlots3_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )

/*
4.初始化qt_meta_stringdata_SignalsAndSlots3,並且將所有函式拼接成字串,中間用\0分開
*/
static const qt_meta_stringdata_SignalsAndSlots3_t qt_meta_stringdata_SignalsAndSlots3 = {
    {
QT_MOC_LITERAL(0, 0, 16), // "SignalsAndSlots3" (索引,偏移量,偏移長度),類名
QT_MOC_LITERAL(1, 17, 8), // "sigPrint"
QT_MOC_LITERAL(2, 26, 0), // ""
QT_MOC_LITERAL(3, 27, 4), // "text"
QT_MOC_LITERAL(4, 32, 8) // "sltPrint"
    },
    "SignalsAndSlots3\0sigPrint\0\0text\0"//注意這是一行字串
    "sltPrint"
};
#undef QT_MOC_LITERAL

/*
3.儲存元物件資訊,包括訊號和槽機制、執行時型別資訊和動態屬性系統
*/
static const uint qt_meta_data_SignalsAndSlots3[] = {

 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       2,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount

 // signals: name, argc, parameters, tag, flags
              1,    1,   24,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
              4,    1,   27,    2, 0x0a /* Public */,

 // signals: parameters
    QMetaType::Void, QMetaType::QString,    3,

 // slots: parameters
    QMetaType::Void, QMetaType::QString,    3,

       0        // eod
};

/*
2.執行物件所對應的訊號或槽,或查詢槽索引
*/
void SignalsAndSlots3::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        SignalsAndSlots3 *_t = static_cast<SignalsAndSlots3 *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->sigPrint((*reinterpret_cast< const QString(*)>(_a[1]))); break;
        case 1: _t->sltPrint((*reinterpret_cast< const QString(*)>(_a[1]))); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        void **func = reinterpret_cast<void **>(_a[1]);
        {
            typedef void (SignalsAndSlots3::*_t)(const QString & );
            if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&SignalsAndSlots3::sigPrint)) {
                *result = 0;
                return;
            }
        }
    }
}

/*
1.首先初始化靜態變數staticMetaObject,併為QMetaObject中的無名結構體賦值
*/
const QMetaObject SignalsAndSlots3::staticMetaObject = {
    { &QObject::staticMetaObject, //靜態變數地址
      qt_meta_stringdata_SignalsAndSlots3.data,
      qt_meta_data_SignalsAndSlots3,  
      qt_static_metacall, //用於執行物件所對應的訊號或槽,或查詢槽索引
      Q_NULLPTR, 
      Q_NULLPTR
    }
};

  從上面的程式碼中,我們得知Qt的元物件系統:訊號槽,屬性系統,執行時類資訊都儲存在靜態物件staticMetaObject中。

  接下來是對另外三個公有介面的定義,在你的程式碼中也可以直接呼叫下面的函式哦。

//獲取元物件,可以呼叫this->metaObject()->className();獲取類名稱
const QMetaObject *SignalsAndSlots3::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

//這個函式負責將傳遞來到的類字串描述,轉化為void*
void *SignalsAndSlots3::qt_metacast(const char *_clname)
{
    if (!_clname) return Q_NULLPTR;
    if (!strcmp(_clname, qt_meta_stringdata_SignalsAndSlots3.stringdata0))
        return static_cast<void*>(const_cast< SignalsAndSlots3*>(this));
    return QObject::qt_metacast(_clname);
}

//呼叫方法
int SignalsAndSlots3::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 2)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 2;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 2)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 2;
    }
    return _id;
}

  接下來,我們發現在標頭檔案中宣告的訊號,其真正定義是在這裡,這也是為什麼signal不需要我們定義的原因。

// SIGNAL 0
void SignalsAndSlots3::sigPrint(const QString & _t1)
{
    void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

關鍵字

signals

# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)
# define signals Q_SIGNALS

  看到了嗎,如果signals被展開的話就是public,所以所有的訊號都是公有的,也不需要像槽一樣加public,protected,private的限定符。

slots

# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define slots Q_SLOTS

slots和signals一樣,只是沒有了限定符,所以它是否可以被物件呼叫,就看需求了。

emit

  它的巨集定義:# define emit

  emit後面也沒有字串!當它被替換的時候,程式其實就是呼叫了sigPrint()函式,而不是真正意義上的傳送一個訊號,有很多初學者都是認為當emit的時候,Qt會發訊號,所以才會有很多人問“當emit之後,會不會立即執行其後面的程式碼”。當然,如果想讓emit後面的程式碼不需要等槽函式執行完就開始執行的話,可以設定connect第5個引數。

  Qt之所以使用# define emit,是因為編譯器並不認識emit啊,所以把它定義成一個空的巨集就可以通過編譯啦。

訊號槽的呼叫流程

  好,通過以上的程式碼和分享我們來總結一下具體流程。

  moc查詢標頭檔案中的signals,slots,標記出訊號和槽。將訊號槽資訊儲存到類靜態變數staticMetaObject中,並且按宣告順序進行存放,建立索引。當發現有connect連線時,將訊號槽的索引資訊放到一個map中,彼此配對。當呼叫emit時,呼叫訊號函式,並且傳遞傳送訊號的物件指標,元物件指標,訊號索引,引數列表到active函式。通過active函式找到在map中找到所有與訊號對應的槽索引根據槽索引找到槽函式,執行槽函式。以上,便是訊號槽的整個流程,總的來說就是一個“註冊-索引”機制,並不存在傳送系統訊號之類的事情。

  注意,我們本節講的東西都是以connect第五個引數是預設為前提的。

  Qt通過訊號和槽機制,使得程式設計師在建立類時可以只關注類要做什麼,這使得一個類真正具有了獨立性。訊號槽讓它們之間自由的,動態的進行互動,從而使整個系統執行流暢,而你也不需要再插手管理。