军工物联网技术C模拟实现Qt的信号与槽机制学到牛牛
作者:学到教育 任金城
对于大多学习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
#include
#include
#include
#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 sigs;
std::vector 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 mp; //用于保存信号索引与接收者对象即索引的对应关系
//由于一个信号可以对应多个槽,同样多个信号也可以对应一个槽,所以这里选用了multimap容器做对应关系映射
};
object.cpp
#include "object.h"
#include //调用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 sigs(sigs_name, sigs_name+1);
static std::vector 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 sigs;
static std::vector 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源码做深入学习。
微信外卖的骗局昨天在刷头条的时候刷到了一个广告,是微信外卖招募代理商的这么一个非常诱人的商机广告,我当时就留下了姓名电话。今天上午的时候就收到了济南总部打来的一个电话,跟我说了这个代理商的佣金收
一文带你搞定TCP挥手摘要TCP断开连接TIMEWAITTIMEWAIT优化TCP保活Sokcet编程TCP断开连接TCP断开连接,需要经历四次挥手,通信的双方都可主动断开连接,断开连接通信的双方占用的
start与run区别死记硬背1start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。2通过调用Thread类的start()方法来启动一
跨境电商如何应对欧盟新税改电商卖家的销售策略要针对性进行调整例如在亚马逊上销售的商品定价,需要加上增值税,以免降低卖家自己的利润。代扣代缴的政策实行后,货物清关时所缴纳的进口增值税,也无法进行抵扣,需要跟税
GooglePhotos的上锁文件夹功能可以在非Pixel手机使用了DoNews12月7日消息(刘文轩)GooglePhotos有一项LockedFolder功能,最初在此前的GoogleIO上公开,并率先提供给Pixel手机用户使用,不过现在这项
89岁老教授状告知网获赔70万不讲理!拿我的知识去赚钱文教育研说家知网一家独大,让多少学生和学者无奈但又无可奈何!知网是干什么的呢?想必大家还记得翟天临这个名字,当时有人问他,关于知网的问题。翟天临直接说,知网是什么。作为一个博士生,
Go里的nil在日常Golang使用中,你有没有这样的疑惑?nil是什么?哪些可以用nil?哪些不能用nil?接下来,我将对这些内容进行总结。一什么是nil首先nil是一个变量,我们可以在源码包
关于SpringCloudAlibaba,看这篇文章就够了!(附教程资料)什么是SpringCloudAlibaba?首先我们需要了解一下SpringCloud,然后再来了解SpringCloudAlibabaSpringCloud源自官方描述Sprin
老程序员3万元接的口罩项目,开发周期10天,真香本项目来自程序汪背后的私活小团队,开发了一个口罩项目,给粉丝分享一下解决方案,希望给想接私活的朋友一些经验参考视频版本在B站我是程序汪另一个口罩项目的案例,他是蓝牙直接跟硬件对接,
分布式定时任务调度框架实践分布式任务调度框架几乎是每个大型应用必备的工具,本文介绍了任务调度框架使用的需求背景和痛点,对业界普遍使用的开源分布式任务调度框架的使用进行了探究实践,并分析了这几种框架的优劣势和
Go语言核心36讲(Go语言实战与应用二十二)学习笔记44使用os包中的API(上)我们今天要讲的是os代码包中的API。这个代码包可以让我们拥有操控计算机操作系统的能力。前导内容os包中的API这个代码包提供的都是平台不相关的API