简 述: Qt 的信号和槽原理分析:手写一个 moc 预编译器模拟生成 mo_xxx.cpp 过程

[TOC]


本文初发于 “偕臧的小站” ifmet.cn,同步转载于此。


背景

   最近手工模拟了 Qt 的信号和槽实现原理,用纯 C++ 实现来实现一个 connet 函数。我的 💻 环境为: uos20 amd64 📎 Qt 5.11.3 📎 gcc/g++ 9.0 📎 gdb8.0

Object::db_connet(obj1, "sig1()", obj2, "slot1()");

原理

     Qt 的信号和槽解决了 GUI 开发过程中 “对象间通信和共享数据” 问题。属于 Qt 的一个独创解决思路。关于为啥不用 C++ 的标准智能指针或者其它库呢??那时候 Boost 和 stl 都没诞生呢。所以代价就是通过 moc 预编译器来扩展 C++ 语法。

​ 模拟 object.h 通过 moc 生成 db_object.cpp 的过程中,要想实现如下的一个 connet 函数,很重要就是解决 “A 对象的 n 信号,映射关联到 B 对象的 m 槽函数”

static void db_connet(Object *sender, const char *sig, Object *receiver, const char *slot);

object.h

下图 的object.h 模拟 QObject 类;

  • 自定义 C++ 之外的关键字 db_signalsdb_slotsdb_emit;编译器不报错的?添加为宏预定义处理即可。
  • MetaObject 对象模拟是元对象,用来在 object 中用来存储 db_signalsdb_slots 标记下面的函数名;对于信号,只需要声明,则不需要定义(通过 moc 自动在db_object.cpp 中生成 ),但是槽函数则需要自己在其它函数中声明和定义
  • Connection 是做作为 ConnectionMap 的 value 使用的。保存本对象所链接的对象和对应的槽函数
  • ConnectionMap “对象 + 信号 ——–映射——> 对象 + 槽” 在代码中存储表现形式。
  • ConnectionMapIt 是 ConnectionMap 遍历的游标。
  • static MetaObject meta 具体的元对象。
  • metacall 通过槽的索引 ==> 槽函数,然后调用
  • static void db_connet() 静态函数设计,可在任何地方都可以使用,连接信号和槽函数
  • void testSignal() 测试信号函数,看槽函数是否会成功被调用,即 emit signal( )
#ifndef OBJECT_H
#define OBJECT_H

#include <iostream>
#include <map>
using namespace std;

#define db_signals protected
#define db_slots
#define db_emit

class Object;
struct MetaObject              // 元对象
{
    const char *sig_names;
    const char *slot_names;

    static void active(Object *sender, int idx);
};

struct Connection
{
    Object *recviver;
    int method;
};

typedef multimap<int, Connection> ConnectionMap;
typedef multimap<int, Connection>::iterator ConnectionMapIt;

class Object
{
    static MetaObject meta;
    void metacall(int idx);

public:
    static void db_connet(Object *sender, const char *sig, Object *receiver, const char *slot);
    void testSignal();

public:
    Object();
    virtual ~Object();

db_signals:
    void sig1();
//    void sig2();

public db_slots:
    void slot1();
//    void slot2();

friend class MetaObject;
private:
    ConnectionMap connectionsMap;
};

#endif // OBJECT_H

object.cpp

object 类的实现细节

#include "object.h"
#include <cstring>

static int findSignalIndex(const char *str, const char *subStr)
{
    if (!str || !subStr || strlen(str) < strlen(subStr))
        return -1;

    int ret = strcmp(str, subStr);
    if (ret == 0)
        return ret;
    else
        return -1;

}

void Object::db_connet(Object *sender, const char *sig, Object *receiver, const char *slot)
{
    int sig_idx = findSignalIndex(sender->meta.sig_names, sig);
    int slot_idx = findSignalIndex(receiver->meta.slot_names, slot);

    if (sig_idx == -1 || slot_idx == -1) {
        cout<<"signal or slot not found!";
        return;
    } else {
        Connection c = {receiver, slot_idx};
        sender->connectionsMap.insert(pair<int, Connection>(sig_idx, c));  // connectionsMap 私有成员
    }
}

void Object::testSignal()
{
    db_emit sig1();
}

Object::Object()
{

}

Object::~Object()
{

}

// 通过 sender 的信号 idx ==> 槽函数
void MetaObject::active(Object *sender, int idx)
{
    pair<ConnectionMapIt, ConnectionMapIt> ret;
    ret = sender->connectionsMap.equal_range(idx);  // 寻找[idx,  )
    for (ConnectionMapIt it = ret.first; it != ret.second; ++it) {
        Connection c = (*it).second;
        c.recviver->metacall(idx);
    }

}

db_object.cpp

object 类通过 moc 生成 db_object.cpp,一些 moc 所执行的代码扩展在 db_object.cpp 里面。

#include "object.h"

//db_object: 是由 moc 编译器 将 object.cpp 展开的内容(此处手写表示)

const char sig_names[] = "sig1()";
const char slot_names[] = "slot1()";
MetaObject Object::meta = {sig_names, slot_names};

void Object::sig1()
{
    MetaObject::active(this, 0);
}

void Object::slot1()
{
    cout << "-----------> this is slot1()";
}

// 槽的索引==> 槽函数
void Object::metacall(int idx)
{
    switch (idx) {
    case 0:
        slot1();
        break;
    default:
        break;
    }
}

main.cpp

#include <iostream>
#include "object.h"
using namespace std;

// 目的:自行构造 moc 编译器,手动将 object.h --> db_bject.cpp (宏 和 moc 编译器处理的部分)
// 时间:2021-03-26
// 下载:https://github.com/xmuli/QtExamples

int main(int argc, char *argv[])
{
    Object *obj1 = new Object();
    Object *obj2 = new Object();

    Object::db_connet(obj1, "sig1()", obj2, "slot1()");
    obj1->testSignal();
    return 0;
}

// 终端输出打印:
----------> this is slot1()

总结

本文参考 文一文二 ,但实际发现其源码之间有几处错误,实际运行失败,参考思路重写,方便后来者学习,且本文还有改进之处,日后有空改进,修改点如下:

  • findSignalIndex 函数重写,使得能够识别信号和槽函数的重载函
  • db_connet 在重构,支持宏方式(Qt4)和 函数指针(Qt5)方式;(当前为使用字符串作为参数)
  • 构建 xxx_p.h,将成员变量都放在此文件中,加快项目的编译速度。

系列地址

QtExamples 【DbSigSlot】

欢迎 starfork 这个系列的 QT / DTK 学习,附学习由浅入深的目录。