2.3信号和槽(下)

简介: 2.3信号和槽(下)

今天我们终于可以看看神奇的信号槽是怎么实现的了。话不多说,直接上代码。

2.3.1 示例程序

新建控制台应用程序,再添加一个新类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字样。

2.3.2 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.cppmoc_signalsandslots3.cpp作为源文件一起进行编译。

由此可知,Qt又额外生成了moc_signalsandslots3.cpp文件,其名称为,同名源文件前加上了moc前缀。

2.3.3 moc预编译器

moc(Meta-Object Compiler)元对象预编译器。

moc读取一个c++头文件。如果它找到包含Q_OBJECT宏的一个或多个类声明,它会生成一个包含这些类的元对象代码的c++源文件,并且以moc_作为前缀。

信号和槽机制、运行时类型信息和动态属性系统需要元对象代码。

由moc生成的c++源文件必须编译并与类的实现联系起来。

通常,moc不是手工调用的,而是由构建系统自动调用的,因此它不需要程序员额外的工作。

2.3.4 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一起编译的原因了。否则,这个类是不完整的,那肯定也是不可能编译通过的。


2.3.5 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);
}

2.3.6 关键字

2.3.6.1 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的限定符。

2.3.6.2 slots

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

slots和signals一样,只是没有了限定符,所以它是否可以被对象调用,就看需求了。

2.3.6.3 emit

它的宏定义:# define emit

emit后面也没有字符串!当它被替换的时候,程序其实就是调用了sigPrint()函数,而不是真正意义上的发送一个信号,有很多初学者都是认为当emit的时候,Qt会发信号,所以才会有很多人问“当emit之后,会不会立即执行其后面的代码”。当然,如果想让emit后面的代码不需要等槽函数执行完就开始执行的话,可以设置connect第5个参数。

Qt之所以使用# define emit,是因为编译器并不认识emit啊,所以把它定义成一个空的宏就可以通过编译啦。

2.3.7 信号槽的调用流程

好,通过以上的代码和分享我们来总结一下具体流程。

  1. moc查找头文件中的signals,slots,标记出信号和槽
  2. 将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
  3. 当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
  4. 当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
  5. 通过active函数找到在map中找到所有与信号对应的槽索引
  6. 根据槽索引找到槽函数,执行槽函数。

以上,便是信号槽的整个流程,总的来说就是一个“注册-索引”机制,并不存在发送系统信号之类的事情。

注意,我们本节讲的东西都是以connect第五个参数是默认为前提的。

Qt通过信号和槽机制,使得程序员在创建类时可以只关注类要做什么,这使得一个类真正具有了独立性。信号槽让它们之间自由的,动态的进行交互,从而使整个系统运行流畅,而你也不需要再插手管理。

欢迎关注小豆君的微信公众号:小豆君,只要关注,便可加入小豆君为大家创建的C++\Qt交流群,方便讨论学习。


相关文章
|
7天前
|
传感器 安全
第四问:QT中信号和槽原理
Qt的信号与槽机制是观察者模式的典型实现,允许对象间通信而不直接依赖。信号用于通知事件发生,槽是响应信号的函数,通过`QObject::connect()`连接。这种机制实现了松耦合、灵活扩展和自动通知,适用于UI更新和数据绑定等场景。
26 1
|
1月前
|
C++
003 Qt_信号和槽-上
本文介绍了Qt中的信号与槽机制,包括信号和槽的概念、本质及连接方法,并演示了如何自定义槽函数。信号是事件的体现,槽是对信号的响应函数。通过信号与槽,可以将独立的控件关联起来,实现复杂的交互逻辑。文中还详细展示了如何在Qt项目中定义和使用槽函数,通过实例代码和图形化界面操作,帮助读者更好地理解和应用这一机制。
56 1
003 Qt_信号和槽-上
|
6月前
|
安全 C++ Windows
Qt信号与槽机制
Qt信号与槽机制
58 1
|
6月前
|
C++
Qt信号和槽
Qt信号和槽
|
7月前
|
编译器 API
【Qt】- 信号和槽函数
【Qt】- 信号和槽函数
|
7月前
|
算法 编译器
[C++&Qt] 通过信号与槽传递数据
[C++&Qt] 通过信号与槽传递数据
187 0
09 QT - 信号和槽机制
09 QT - 信号和槽机制
45 0
|
开发框架 安全 编译器
2.3信号和槽(上)
2.3信号和槽(上)
2.3信号和槽(上)