欢迎来到军工软件开发人才培养基地——学到牛牛

C++模拟实现Qt的信号与槽机制

时间:2024-05-06 07:01:10 来源:学到牛牛

​对于大多学习Qt的朋友,心中都有种好奇——那就是Qt最核心的信号与槽是如何实现的,对于小编自己也是一样,当然大家肯定都会去查阅相关资料,但大部分时候也只是一知半解,如果说要自己实现就会又摸不着头脑了;所以小编决定自己亲自用C++实现一个简单版的信号槽,来理解Qt的实现原理。于是小编就在翻阅各牛人朋友的博客和反复研究Qt源码自己重新写了一下以便交流学习。

我们先还是简单的梳理一下Qt信号与槽的实现机理:在Qt中实现信号与槽最重要的就是通过元对象系统(MOS)的元对象编译器(MOC)将我们定义的需要使用到信号与槽的类中的信号及信号调用槽函数的方法进行定义(这一步就会生成与源文件对应的moc_xx.cpp文件),然后通过系统提供的关联方法(connect)将信号与槽建立一一对应关系,当发射信号(其实就是调用信号函数)时就会通过信号与槽的对应关系找到对应槽函数进行调用。这样的好处就是对于使用者而言不必去关心函数指针回调函数这些对于初学者比较不太容易搞清晰的东西,简化了使用者的操作。当然就像我们在享受幸福生活的时候,就一定有人在我们背后默默付出砥砺前行!这里也一样,对于我们使用者简化了操作,那为了实现这样的效果就需要在后台提供更多的支持。接下来我们就通过代码再来梳理一遍。

首先我们使用信号与槽肯定就会有信号的发送者与接收者,所以我们就先去定义这两个类对象:

sender.h

#pragma once

#include "object.h"

 

class Sender : public Object

{

    X_OBJECT

public:

    Sender(int n = 0) : m_num(n){

    }   

    void sendSig(); 

signals:

    void holdClass(int n); 

    int m_num;

};

sender.cpp

#include "sender.h"

 

void Sender::sendSig()

{

std::cout << "发送信号:holdClass" << std::endl;

    emit holdClass(m_num);

}

在Qt中需要使用信号槽的对象都需要直接或间接继承一个类QObject,并且需要添加一个私有宏定义Q_OBJECT,这里就用Object和X_OBJECT代替,signals是Qt中用于声明信号函数的关键字,emit是Qt中用于发送信号定义的关键字,这里我们先假设已经有这些类和宏定义,注意信号函数是不需要我们定义的,他是在MOC预处理生成的moc_xx.cpp中自动生成定义的,所以这里的cpp很简单只有一个普通函数sendSig()的定义。同理我们再自己定义一个信号的接收者对象和其对应的槽函数。

receiver.h

#pragma once

#include "object.h"

 

class Receiver : public Object

{

    X_OBJECT

public:

    Receiver()  {

    }   

public slots:

    void  attendClass(int n); 

};

receiver.cpp

#include "receiver.h"

 

void Receiver::attendClass(int n)

{

    std::cout << "执行槽函数attendClass:cur class " << n << std::endl;

}

这里的slots就是Qt中用于标识槽函数声明的关键字,槽函数是需要用户自己定义的。

然后我们就需要再将发送者信号与接收者槽关联起来,我们这就提供一个主函数来模拟关联信号与槽,让发送者产生信号:

main.cpp

#include "sender.h"

#include "receiver.h"

 

int main()

{

    Sender xuedao(9527);

    Receiver rjc;

    Object::connect(&xuedao, SIGNAL(holdClass(int)), &rjc, SLOT(attendClass(int)));

    xuedao.sendSig();

 

    return 0;

}

这里的SIGNAL与SLOT在Qt中就是两个转换字符串的宏定义,connect是QObject的一个静态函数方法。

我们要想这个程序能正常运行起来,接下来我们就需要去定义一个类似QObject的Object类和上面需要用到的关键字与宏定义,以及模拟MOC预处理产生对应的moc_xx.cpp,里面细节的地方为了方便理解我都通过代码注释解释说明了

object.h

#pragma once

 

#include <iostream>

#include <map>

#include <vector>

#include <string>

#define signals protected

#define slots 

#define emit 

#define SLOT(slt)    "1"#slt      // 1用于标识槽函数

#define SIGNAL(sig)  "2"#sig      //2用于标识信号

 

class Object;

struct MetaObject

{

    //每个对象可能会有多个信号与槽函数,这里就用两个vector分别保存信号与槽函数信息操作起来方便点

    std::vector<std::string> sigs;

    std::vector<std::string> slts;

    //activate的功能是通过信号发送者即信号索引找到关联接收者和方法索引并调用对应方法

    static void activate(Object *sender, int idx, void **argv);  //void **argv对应信号传递的参数

    struct Connection   //用于打包信号接收者与方法的索引(对应上面定义的vector中的信号槽的索引)

    {   

        Object *m_receiver;

        int method;

    };

};

//Q_OBJECT宏中定义的比较多这里只选择了我们需使用的几个

//static MetaObject meta用于保存使用该宏定义对象中的信号与槽信号与槽的相关信息 

//getMetaObject()用于返回发送者或接收者对象中的static MetaObject meta对象

#define X_OBJECT static MetaObject meta;                \

                virtual MetaObject *getMetaObject();    \

                virtual void metaCall(int idx, void **argv);  //idx为对应槽函数的索引,void**argv用于接收信号传递的参数

class Object   //需要使用信号槽对象的公共接口对象  

{

    X_OBJECT

public:

    virtual ~Object() {}

    //connect用于建立信号与槽的关联信息

    static void connect(Object *sender, const char *s1, Object *receiver, const char *s2);

private:

    friend class MetaObject; //用于方便meta对象访问下面的信号槽map

    std::multimap<int, MetaObject::Connection> mp; //用于保存信号索引与接收者对象即索引的对应关系

    //由于一个信号可以对应多个槽,同样多个信号也可以对应一个槽,所以这里选用了multimap容器做对应关系映射

};

object.cpp

#include "object.h"

#include <string.h>  //调用strcmp函数需要包含

 

void MetaObject::activate(Object *sender, int idx, void **argv)

{

    //在信号槽对应关系的mp中找到发送者idx索引信号对应的接收者及关联方法的调用

    auto ptr = sender->mp.equal_range(idx);

    for(auto it = ptr.first; it != ptr.second; it++) {

        MetaObject::Connection con = it->second;

        con.m_receiver->metaCall(con.method, argv);  //调用接收者与发送者信号关联的方法,并传递需要的参数

    }

}

 

void Object::connect(Object *sender, const char *s1, Object *receiver, const char *s2)

{

    int sig_idx = -1, slt_idx = -1;

    MetaObject *senderMeta = sender->getMetaObject();       //获取发送者中保存的meta对象

    MetaObject *receiverMeta = receiver->getMetaObject();   //获取接收中保存的meta对象

    //比对信号名称找到对应的信号索引

    for(int i = 0; i < senderMeta->sigs.size(); i++) {

        if(0 == strcmp(s1+1, senderMeta->sigs[i].c_str())) {

            sig_idx = i;

        }

    }

//这里确认是槽函数,并找到对应的槽函数索引

//如果有信号与信号关联的情况这里就需要去查找接收者对应的信号索引,这里省略了

if('1' == *s2) {   

        for(int i = 0; i < receiverMeta->slts.size(); i++) {

            if(0 == strcmp(s2+1, receiverMeta->slts[i].c_str())) {

                slt_idx = i;

            }

        }

    }

    if(-1 == sig_idx || -1 == slt_idx) {

        std::cout << "no match sig or slt" << std::endl;

    }

    //利用multimap建立信号索引与接收者和方法索引的对应关系

    MetaObject::Connection con = {receiver, slt_idx};

    sender->mp.insert(std::make_pair(sig_idx, con));

}

//下面的主要是预留的方便父类调用子类重写方法的接口这里简单定义即可

void Object::metaCall(int idx, void **ag)

{

}

MetaObject Object::meta;

MetaObject *Object::getMetaObject()

{

    return &meta;

}

下面就轮到MOC生成的moc_xx.cpp,这些文件在Qt中是自动生成的不需要我们实现,我这里只能手动模拟简单的实现发送者的moc_sender.cpp与接收者的moc_receiver.cpp最终我们编译程序是需要将这两个文件一起编译才能通过的。

moc_sender.cpp

#include "sender.h"

 

//根据定义的信号槽顺序将信号与槽函数名称进行保存,Qt中会将函数名称参数分开保存处理,这里简单模拟以下就好

static const char *sigs_name[] = {"holdClass(int)"};   

static const char *slts_name[] = {nullptr}; //空表示当前没有定义对应的函数  

static std::vector<std::string> sigs(sigs_name, sigs_name+1);

static std::vector<std::string> slts;

MetaObject Sender::meta = {sigs, slts};

//Sender的信号定义

void Sender::holdClass(int n)  

{

    void *arg[] = {(void *)&n};

    //调用MetaObject的静态方法activate传递当前的信号发送者对象、信号索引及参数

    MetaObject::activate(this, 0, arg);  //0表示当前信号函数在sigs_name[]中的索引

}

MetaObject *Sender::getMetaObject()

{

    return &meta;  //返回Sender的meta对象

}

 

void Sender::metaCall(int idx, void **arg)   

{

    // 我们这里Sender 中没有槽函数所以这里没任何操作

}

 

moc_receiver.cpp

#include "receiver.h"

 

static const char *sigs_name[] = {nullptr};  

static const char *slts_name[] = {"attendClass(int)"};

static std::vector<std::string> sigs;

static std::vector<std::string> slts(slts_name, slts_name+1);

 

MetaObject Receiver::meta = {sigs, slts}; 

 

MetaObject *Receiver::getMetaObject()

{

    return &meta;   //返回Receiver的meta对象

}

 

void Receiver::metaCall(int idx, void **arg)

{   

    //这里根据slts_name[]中的索引值调用对应的槽函数

    if(0 == idx) {

        int n = *((int *)arg[0]);

        attendClass(n);

    }   

}

有了上面这些文件最后我们只需要将所有的.cpp文件一起编译运行就可以实现Qt中信号与槽的效果了:

g++ object.cpp sender.cpp receiver.cpp moc_sender.cpp moc_receiver.cpp main.cpp -o xuedao

也可用其他可使用的编译器编译进行编译,这里直接用的g++。

另外如果某个对象修改或增删了信号或槽就需要去手动修改对应的moc_xx.cpp文件即可,Qt中实现考虑的实际问题会更多,这里只是把整个信号槽关联及调用流程框架进行了梳理,具体的大家可以参考Qt源码做深入学习。