一、写在前面
最近由于工作的需要,想着使用Qt的信号槽机制,但又不依赖Qt,所以想着实现一下,不过在这里要说明:
1. 在Boost库中是有信号槽的如果你不想太麻烦,可以直接使用这里提供的功能
2. Qt的信号槽实现机制是很复杂的,我们之所以可以使用Qt的关键字来实现相关功能,是因为qt有自己的预处理器,在编译的时候预处理器会帮助我们完成很多事情,而这些事情不用我们去关心,关于Qt的预处理器目前没有太多的了解,这里不做讲解,在这里推荐先看看 这篇博客(很不错!) ,这篇博客写的很好,给了我很大的启发
二、实现
凡事都是有简单到复杂的,所以这里给出我的几个版本,希望对大家有帮助,在网上也有很多前辈对这一功能进行了自己的实现,各有千秋,所以可以自行百度查找,这里不再一一列出,当然如果时间和经历允许,可以通过研究qt的源代码来学习,qt的源代码还是很有意思的(我只看来一小部分)。我一共写了三个版本,下面依次介绍不同版本,每个版本的所有代码粘贴在一个文件中就可以运行了,所以我就不放全家福了
版本1无参信号槽
首先来实现一个不含参数的信号槽连接,实现简单的回调,但是在这个简单的模式当中不会对信号槽的处理进行隐藏,处理函数需要手动补全,在版本3中会对这部分进行隐藏,不要急~这个版本只是为了初步理顺处理流程!!!
1. 头文件
#include <vector>
#include <iostream>
2. 为了存放每一对信号槽匹配关系,所以需要建立一个容器,由于具体槽的类型不一样,所以通过一个父类来隐藏子类差异,首先给出这一父类,其中有成员变量id,通过此id来找到不同的连接对,是不同连接对的唯一标识,这一对象应该对每一个具体对象来管理,可以按照id来控制触发顺序
class EmelemtBase
{
public:
virtual void HitTarget() = 0;
unsigned int GetId() { return id; }
unsigned int id;
};
3. 下面来创建存储容器,容器继承自ElementBase,在这个里面来存放具体的信号槽以及信号和槽所属的对象,记录的内容一共四项
template<typename C1,typename T1,typename C2,typename T2>
class Element:public EmelemtBase
{
public:
virtual void HitTarget()
{
(targetBody->*targetPtr)();
}
virtual void SetTarget(T1 s, C2 tar,T2 p, const unsigned int& i)
{
sourcePtr = s;
targetBody = tar;
targetPtr = p;
id = i;
}
C1 sourceBody; //信号主体
T1 sourcePtr; //存储信号函数指针
C2 targetBody; //目标主体
T2 targetPtr; //存储目标函数指针
};
4. 为了实现信号槽,要求所有对象都要记录属于自己的信号槽记录表单,所有创建公共父类,在父类中通过一个vector来存储该对象的信号槽表单
/*
对象的基类,所有的对象从这一父类进行派生,用来统一信号槽的后台存储
*/
class ThreadBase
{
public:
ThreadBase() {}
virtual ~ThreadBase()
{
std::vector<EmelemtBase*>::iterator iter = ElementManager.begin();
for (; iter != ElementManager.end(); ++iter)
{
if ((*iter) != nullptr)
{
delete (*iter);
(*iter) = nullptr;
}
}
}
std::vector<EmelemtBase*> ElementManager;
};
5. 这时我们可以对连接函数进行实现,这里写一个模板函数,接口中包含5个参数
template<typename C1, typename T1, typename C2, typename T2>
void myconnect(C1 obj1, T1 signal, C2 obj2, T2 slot,unsigned int id)
{
Element<C1,T1,C2,T2>* tempElement = new Element<C1, T1, C2, T2>();
EmelemtBase* tempElementPtr = tempElement;
tempElement->SetTarget(signal,obj2,slot, id);
obj1->ElementManager.push_back(tempElementPtr);
}
/*连接函数,改变接口,这一对象在后面的版本中会十分重要,在这里很鸡肋*/
#define CONNECT(obj1,signal, obj2, slot, id) \
myconnect(obj1,signal, obj2, slot, id);
6. 实例化一个信号类,这里是最初的,所以不对信号处理进行隐藏封装,调用信号实际上是触发一个函数,这个函数体中实现了处理方法,不过这是最初的,不是最终的!!!不然你写一个信号,就得写一个处理方法,岂不是很累?
/*第一种对象*/
class Unit1 :public ThreadBase
{
public:
void signal1()
{
std::vector<EmelemtBase*>::const_iterator iter = ElementManager.begin();
for (;iter != ElementManager.end();++iter)
{
if ((*iter)->GetId() == 1)
{
(*iter)->HitTarget();
}
}
}
void run()
{
signal1();
}
};
7. 实例化一个槽类,槽不需要实现相应处理方法,因为这是被调用的,所以直接正常写就行了。
/*第二种对象*/
class Unit2 :public ThreadBase
{
public:
void slot1()
{
std::cout << "hited" << std::endl;
}
};
8. 最给个main函数示例,第一版就结束啦~运行结束是在控制台上输出hited
int main()
{
Unit1 obj1;
Unit2 obj2;
CONNECT(&obj1, &Unit1::signal1, &obj2, &Unit2::slot1,1);
obj1.run();
system("pause");
return 0;
}
版本2——含参数传递
在这个版本中要实现一个可以传递参数的信号槽,由于函数参数类型是不确定的,所以这里使用一个any类型(boost库中有),这里我用的是我学习时候写的一个自己的MyAny类型,实现原理参考boost库中的any类型
1. 先给头文件
#include <vector>
#include <iostream>
#include <stdarg.h>
//----------------------
#include "MyAny.h"
//-----------------------
2. 连接基类,同版本1
class EmelemtBase
{
public:
virtual void* HitTarget(std::vector<any>& pl) = 0;
unsigned int GetId() { return id; }
unsigned int id;
};
3. 连接记录容器,同版本1
template<typename C1, typename T1, typename C2, typename T2>
class Element :public EmelemtBase
{
public:
virtual void* HitTarget(std::vector<any>& pl)
{
(targetBody->*targetPtr)(pl);
return nullptr;
}
virtual void SetTarget(T1 s, C2 tar, T2 p, const unsigned int& i)
{
sourcePtr = s;
targetBody = tar;
targetPtr = p;
id = i;
}
C1 sourceBody;
T1 sourcePtr;
C2 targetBody;
T2 targetPtr;
};
//=================================================================================
4. 对象基类,同版本1
class ThreadBase
{
public:
ThreadBase() {}
virtual ~ThreadBase()
{
std::vector<EmelemtBase*>::iterator iter = ElementManager.begin();
for (; iter != ElementManager.end(); ++iter)
{
if ((*iter) != nullptr)
{
delete (*iter);
(*iter) = nullptr;
}
}
}
std::vector<EmelemtBase*> ElementManager;
};
5. 连接函数,同版本1
template<typename C1, typename T1, typename C2, typename T2>
void myconnect(C1 obj1, T1 signal, C2 obj2, T2 slot, unsigned int id)
{
Element<C1, T1, C2, T2>* tempElement = new Element<C1, T1, C2, T2>();
EmelemtBase* tempPtr = tempElement;
tempElement->SetTarget(signal, obj2, slot, id);
obj1->ElementManager.push_back(tempPtr);
}
//=================================================================================
6. 连接接口宏封装,不同与版本1!!! 在这里我们对槽的处理进行隐藏,在这里,通过MySLOT宏来声明一个函数可以作为这个类的槽来使用,实际上是通过这个宏来创建一个*hide函数,这一函数是对参数进行转换,参数在信号和槽之间的传递,首先信号将参数装入一个any类型容器,传给隐藏槽,隐藏槽将参数转化成具体类型,在传给具体槽,这样隐藏槽可通过宏来展开,不用反复书写,只需手动声明即可,在MySLOT宏中需要传入两个参数,槽名和参数类型(这里只允许一个参数)。此外要注意CONNECT宏的书写,很关键的哦~
#define CONNECT(obj1,signal, obj2, slot, id) \
myconnect(obj1,signal, obj2, slot##Hide, id);
//=================================================================================
#define MySLOT(FunName,ParameterType) \
inline void FunName##Hide (std::vector<any>& pl)\
{\
FunName(any_cast<ParameterType>(pl.at(0))); \
}
//=================================================================================
7. 信号类,现在signal可以带有一个自己的参数了,不过信号体依然是裸露的,没有进行隐藏。
class Unit1 :public ThreadBase
{
public:
void signal1(const std::string& txt)
{
std::vector<any> pl;
pl.push_back(txt);
std::vector<EmelemtBase*>::const_iterator iter = ElementManager.begin();
for (; iter != ElementManager.end(); ++iter)
{
if ((*iter)->GetId() == 1)
{
(*iter)->HitTarget(pl);
}
}
}
void run()
{
signal1("hello world");
}
};
//=================================================================================
8. 槽类,为了更容易理解宏的作用,在这里给出了我的实验时的代码,如果用注释掉的代码,就不用写MySLOT宏了,由于没有向Qt预处理器一样的自已的预处理,所以在这里采用手动注册的方式来注册槽,也就是说如果你想让一个函数当作槽函数来使用,那么你就要手动写明类似`MySLOT(slot1,std::string)`的语句
class Unit2 :public ThreadBase
{
public:
//----------- 隐藏函数原型测试---------------
// void slot1Hide(std::vector<any>& pl)
// {
// slot1(any_cast<std::string>(pl.at(0)));
// }
//----------基于隐藏函数的宏测试(单参数)-------------
MySLOT (slot1,std::string)
void slot1(const std::string& txt)
{
std::cout << "hited\t"<<txt.c_str()<< std::endl;
}
};
9. 最后给一个面函数
int main()
{
Unit1 obj1;
Unit2 obj2;
CONNECT(&obj1, &Unit1::signal1, &obj2, &Unit2::slot1, 1);
obj1.run();
system("pause");
return 0;
}
版本3——含参传递,版本2优化
功能改进包括:
1. 添加了signal注册
2. 优化了slot处理
1. 首先是头文件
#include <vector>
#include <iostream>
#include <stdarg.h>
//----------------------
#include "MyAny.h"
//-----------------------
2. 连接基类。同前
class EmelemtBase
{
public:
virtual void* HitTarget(std::vector<any>& pl) = 0;
unsigned int GetId() { return id; }
unsigned int id;
};
3. 记录类型
template<typename C1, typename T1, typename C2, typename T2>
class Element :public EmelemtBase
{
public:
virtual void* HitTarget(std::vector<any>& pl)
{
(targetBody->*targetPtr)(pl);
// return const_cast<void*>(targetBody->*targetPtr);
return nullptr;
}
virtual void SetTarget(T1 s, C2 tar, T2 p, const unsigned int& i)
{
sourcePtr = s;
targetBody = tar;
targetPtr = p;
id = i;
}
C1 sourceBody;
T1 sourcePtr;
C2 targetBody;
T2 targetPtr;
};
//=================================================================================
4. 每个信号可以绑定多个槽,所以为了提升效率,当触发一个信号的时候,只会触发和该信号绑定的若干个槽,所以在这里每一个对象持有一张二维表单,每一行头是信号名,这一行后面记录了属于该信号的槽
class Sender
{
public:
std::string GetName() { return SenderName; }
void HitTarget(std::vector<any>& pl)
{
Parameters = pl;
HitTarget();
}
void HitTarget(void)
{
std::vector<EmelemtBase*>::const_iterator iter = BoxManager.begin();
for (; iter != BoxManager.end(); ++iter)
{
(*iter)->HitTarget(Parameters);
}
}
std::string SenderName;
std::vector<any> Parameters;
std::vector<EmelemtBase*> BoxManager;
};
5. 对象基类,这里要对Manager的类型进行修改,修改为Sender类型
class ThreadBase
{
public:
ThreadBase() {}
virtual ~ThreadBase()
{
std::vector<EmelemtBase*>::iterator iter = BoxManager.begin();
for (; iter != BoxManager.end(); ++iter)
{
if ((*iter) != nullptr)
{
delete (*iter);
(*iter) = nullptr;
}
}
}
Sender* GetSender(const std::string& name)
{
std::vector<Sender*>::iterator iter = SenderManager.begin();
for (;iter!= SenderManager.end();++iter)
{
if (name == (*iter)->GetName())
{
return (*iter);
}
}
Sender* tempSender = new Sender();
tempSender->SenderName = name;
SenderManager.push_back(tempSender);
return tempSender;
}
std::vector<Sender*> SenderManager;
};
6. 连接函数,这里对接口做了修改,不再需要指定连接序号,但这样触发顺序就不能该类,后期可根据需要来修改,要注意CONNECT宏需要做对应的修改
template<typename C1, typename T1, typename C2, typename T2>
void myconnect(C1 obj1, T1 signal, C2 obj2, T2 slot, unsigned int id,const std::string& name)
{
Element<C1, T1, C2, T2>* tempElement = new Element<C1, T1, C2, T2>();
EmelemtBase* tempBox = tempElement;
tempElement->SetTarget(signal, obj2, slot, id);
obj1->GetSender(name)->BoxManager.push_back(tempBox);
}
//=================================================================================
#define CONNECT(obj1,signal, obj2, slot, id) \
myconnect(obj1,signal , obj2, slot##Hide, id,#signal);
7. 名义信号发射宏
#define Emit(signalName, pa) signalName##Hide (std::string(#signalName ),pa);
8. 注册信号的宏,在这里对信号的处理方法进行封装,所以不再需要手动书写实现方法,只需要声明相关的宏就可以了,这里只是实现一个参数的传递。
#define RegistSignal(signalName, paType) void signalName##Hide (std::string& signame, paType pa) \
{ \
std::vector<any> pl; \
pl.push_back(pa); \
std::vector<Sender*>::const_iterator iter = SenderManager.begin(); \
for (; iter != SenderManager.end(); ++iter) \
{ \
std::string t = (*iter)->GetName();\
int a = t.find(signame);\
if(a != std::string::npos)\
{ \
(*iter)->HitTarget(pl); \
} \
} \
}\
void signalName(paType pa){}
//===============================================================
//半成品,为了实现可变参数传递
//#define MySIGNAL(FunName,ParameterNum, ...) \
//inline void FunName##Hide(const std::vector<any>& pl)\
//{\
// va_list pl;\
// va_start(pl,ParameterNum);\
// for (int i; i<ParameterNum; ++i)\
// {\
// FunName(any_cast<>(pl.at[i])); \
// }\
//}
9. 注册槽的宏,同前
#define RegistSLOT(FunName,ParameterType) \
inline void FunName##Hide (std::vector<any>& pl)\
{\
FunName(any_cast<ParameterType>(pl.at(0))); \
}
//半成品,为了实现可变参数写的函数体,函数头存在缺陷,没有传入可变参数,函数体没有可变参数解析
// void EmitSignal(std::string& signame) \
// { \
// std::vector<any> pl; \
// pl.push_back(parameter); \
// std::vector<Sender*>::const_iterator iter = SenderManager.begin(); \
// for (; iter != SenderManager.end(); ++iter) \
// { \
// if ((*iter)->GetName() == "&Unit1::signal1") \
// { \
// (*iter)->HitTarget(pl); \
// } \
// } \
// }
//=================================================================================
10. 信号类,当使用宏的时候,就不用写具体的实现了,通过宏,一句就完事了,这里只有一个参数哦~(你可以根据想法扩展)
class Unit1 :public ThreadBase
{
public:
//----------基于隐藏函数的宏测试-------------
RegistSignal(signal1, const std::string&)
//----------- 隐藏函数原型测试---------------
// void signal1(const std::string& txt){}
// {
// std::vector<any> pl;
// pl.push_back(txt);
// std::vector<Sender*>::const_iterator iter = SenderManager.begin();
// for (;iter!= SenderManager.end();++iter)
// {
// if ((*iter)->GetName() == "&Unit1::signal1")
// {
// (*iter)->HitTarget(pl);
// }
// }
// }
void run()
{
Emit(signal1,"hello world")
}
};
//=================================================================================
11. 槽类,实现了相关槽之后,通过宏注册槽即可
class Unit2 :public ThreadBase
{
public:
//----------- 隐藏函数原型测试---------------
// void slot1Hide(std::vector<any>& pl)
// {
// slot1(any_cast<std::string>(pl.at(0)));
// }
//----------基于隐藏函数的宏测试(单参数)-------------
RegistSLOT(slot1, const std::string)
void slot1(const std::string& txt)
{
std::cout << "hited" << txt.c_str() << std::endl;
}
};
12. main函数示例
int main()
{
Unit1 obj1;
Unit2 obj2;
CONNECT(&obj1, &Unit1::signal1, &obj2, &Unit2::slot1, 1);
obj1.run();
system("pause");
return 0;
}