赞
踩
对于一个有着多个属性的类对象而言,我们通常希望能够对其进行序列化与反序列化,以保存和导入我们记录下来的物体数据。编写这样的代码通常是繁琐的,并且会带来大量冗余。
我们期望能够达到这样的效果,在类中声明变量的时候,能够自动注册相关的信息。在序列化和反序列化的过程中,该变量的值就会自动被解析,而无需额外的编码。
这意味着,我们的代码可以按如下形式编写,达到自动序列化/反序列化的目的:
- class Object
- {
- protected:
- QOpenGLShaderProgram* program = nullptr;
- public:
- REGISTER_SERIALIZE
- Property_Param (Vector3f, m_f3Position, Vector3f(0,0,0))
- Property_Param (Vector3f, m_f3Rotation, Vector3f(0,0,0))
- Property_Param (Vector3f, m_f3Scale, Vector3f(1,1,1))
- Property_Param (bool, m_bCastShadow, true)
- Property_Param (bool, m_bRender, true)
- Property_Param (float, m_fAlpha, 1.0f)
- Property_Param_Func (int, m_nRenderPriority, -1, OnInitRenderPriority)
- Property_Func (string, m_strObjName, OnLoadObj)
- Property (string, m_strName)
- Property (Shape, m_shape )
- Property (string, m_strType)
- Property (int, m_nId )
-
- void OnInitRenderPriority(const int& value);
- void OnLoadObj(const string& name);
-
- virtual void UpdateLocation();
- virtual void Create() = 0;
- virtual void Render() { }
- virtual void Draw(bool bTess = false);
- virtual void Draw(QOpenGLShaderProgram*,bool bTess = false);
- virtual ~Object() { }
- };
特别地,派生类的写法也基本类似:
-
- class PBRObject : public Object
- {
- private:
- void SetImage(const string& name, QImage& image, GLuint& texId);
- public:
- ~PBRObject () override;
-
- GLuint m_nAlbedo = 0;
- GLuint m_nNormal = 0;
- GLuint m_nMaskTex = 0;
-
- QImage m_imgAlbedo;
- QImage m_imgNormal;
- QImage m_imgMask;
-
- Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
- Property_Func (string, m_strNormal, SetImage, m_imgNormal, m_nNormal)
- Property_Func (string, m_strMask, SetImage, m_imgMask, m_nMaskTex)
-
- Property_Param (Vector3f, m_f3Color, Vector3f(1,1,1))
- Property_Param (float, m_fAo, 0.4f)
- Property_Param (float, m_fRough, 0.0f)
- Property_Param (float, m_fMetal, 0.0f)
- Property_Param (bool, m_bFire, false)
- Property_Param (bool, m_bBloom, false)
- Property_Param (bool, m_bSSR, false)
- Property_Param (bool, m_bXRay, false)
- Property_Param (bool, m_bOutline, false)
-
- Model* pModel = nullptr;
-
-
- void Create() override;
- void Render() override;
- };
我采取的方案是宏定义 + 模板编程。
对象的序列化有着多种格式,最为常见的是键值对的存储方式,类似Xml,Json或者flatbuffers(当然我们可以选择将其存为文本格式或二进制格式),相比起直接把所有值按序存储的暴力方式,它的好处在于添加和移除一些对象并不会影响数据的读取,非常适合应用于一个可能需要不断更新的应用。
本文中,选择了Xml作为存储格式。
序列化注册
首先,为了能够把每个变量对象加入到序列化管理,我们有必要定义一个管理类,那么它应该有一个容器,存储所有的变量名字以及对应的数据;具备加入数据的方法;支持数据的序列化与反序列化:
- class CSerializeHelper
- {
- public:
- class BaseObject
- {
- // ...
- }; // 数据格式定义
-
- void PushBack(BaseObject* obj) // 支持添加数据
- {
- // ...
- }
- void Serialize(QDomElement& child) // 序列化
- {
- // ...
- }
- void Deserialize(QDomElement& child) // 反序列化
- {
- // ...
- }
- private:
- list<BaseObject*> listObjs; // 数据容器
- };
BaseObject中记录了每个变量的一些辅助数据。
之后,为了快速在类中注册这一序列化管理类,我们定义如下宏,在类中直接引用即可:
- #define REGISTER_SERIALIZE \
- CSerializeHelper serializeHelper; \
- void Save(QDomElement& child) \
- { \
- serializeHelper.Serialize(child); \
- } \
- void Load(QDomElement& child) \
- { \
- serializeHelper.Deserialize(child); \
- } \
接下来,我们定义变量,以下是最为简单的定义变量的宏:
- #define Property(type, name) \
- type name; \
当我们在类中编写形如:
Property(int,x)
时,我们实际上就得到了如下的代码:
int x;
序列化对象管理
但这也仅仅定义了变量,我们还没有将其加入到序列化管理中。为了管理该对象,首先我们需要考虑到,对象可以有很多类型,所以我们需要使用泛型编程,也就是将CSerializeHelper中容器管理的对象定义为泛型对象。
其中,m_strName记录了变量名字的字符串形式,unique_ptr<T> m_value记录了对象当前的值的引用。
此外,提供getStrValue() 和 setStrValue()的接口来实现泛型数据到字符串之间的转化,便于序列化。
- class BaseObject
- {
- public:
- BaseObject(const string& inName)
- : m_strName(inName) { }
-
- virtual string getStrName() final { return m_strName; }
- virtual string getStrValue() = 0;
- virtual void setStrValue(const string& strValue) = 0;
- virtual ~BaseObject() = 0;
- private:
- string m_strName;
- };
-
- template<typename T>
- class Param : public BaseObject
- {
- public:
- Param() { }
- // ...
- private:
- unique_ptr<T> m_value;
- }
此时,CSerializeHelper中的三个方法可以定义如下:
- class CSerializeHelper
- {
- public:
- // ...
-
- void PushBack(BaseObject* obj)
- {
- listObjs.push_back(obj);
- }
- void Serialize(QDomElement& child)
- {
- for(BaseObject* obj : listObjs)
- {
- child.setAttribute(QString::fromStdString(obj->getStrName()),
- QString::fromStdString(obj->getStrValue()));
- }
- }
- void Deserialize(QDomElement& child)
- {
- for(BaseObject* obj : listObjs)
- {
- QString attributeName = QString::fromStdString(obj->getStrName());
- if(child.hasAttribute(attributeName))
- {
- QString strValue = child.attribute(attributeName);
- obj->setStrValue(strValue.toStdString());
- }
- }
- }
- };
为了能够在定义变量的同时将变量加入序列化管理,我们在变量声明的同时,生成一个变量数据辅助类BaseObject的实例,此时Propery()宏扩展如下(备注:#代表字符串化,##代表连接):
- #define Property(type, name) \
- type name; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper); \
同时,在构造函数中完成相应的操作(把数据加入序列化管理):
- template<typename T>
- class Param : public BaseObject
- {
- public:
- Param(const string& inName, T* inValue, CSerializeHelper& helper)
- : BaseObject(inName), m_value(inValue)
- {
- helper.PushBack(this);
- }
- // ...
- };
此时,对于Property(int, x)而言,我们实际得到了如下代码:
- int x;
- CSerializeHelper::Param<int>* xParam
- = new CSerializeHelper::Param<int>
- ("x", &x, serializeHelper);
序列化实现(字符串和任意类型相互转换)
至此,我们已经在类中注册了序列化管理类,生成保存和导入的函数,并能够将不同类型的变量自动加入管理维护。剩余的一个工作就是实现任意类型到字符串的相互转换。大部分情况下,我们都可以利用stringstream来辅助这一实现:
- template<typename T>
- class Param : public BaseObject
- {
- public:
- string getStrValue() override
- {
- if(m_value)
- {
- stringstream ss;
- ss << *(m_value);
- return ss.str();
- }
- return string();
- }
-
- void setStrValue(const string& strValue) override
- {
- T res;
- stringstream ss(str);
- ss >> res;
- }
- };
对于内置类型,基本上可以直接使用;对于自定义类型,只需重载operator<<和>>,具备比较好的扩展性。例如,对于自定义类型Vector3f,需要添加如下两个运算符重载:
- struct Vector3f
- {
- float x,y,z;
- Vector3f() { }
- Vector3f(float _x,float _y,float _z) :x(_x),y(_y),z(_z) { }
-
- friend ostream& operator<<(ostream& out, const Vector3f& vec)
- {
- out << vec.x << " " <<vec.y << " " << vec.z ;
- return out;
- }
- friend istream& operator>>(istream& in, Vector3f& vec)
- {
- in >> vec.x >> vec.y >> vec.z;
- return in;
- }
- };
但也有一些例外。比如对于我们自定义的枚举类型,我们就无法为其重载运算符。为了照顾这些特殊情况,我们第一个考虑到的可能是模板特例化,而枚举类型是一类类型,并不是单个类型,这意味我们需要对每一个自定义枚举类型做特例化,这样意味着每次我们添加新的自定义枚举类型时,还需要修改框架代码。
实际上,我们需要的是有条件的编译,也就是在条件A时生成函数A,而在条件B时生成函数B。根据类型的不同,来得到不同的字符串转换方法。我们可以利用enable_if特性来实现:
- template<typename T>
- typename enable_if<is_enum<T>::value, T>::type
- strConvert(const string& str)
- {
- int res = strConvert<int>(str);
- return static_cast<T>(res);
- }
- template<typename T>
- typename enable_if<!is_enum<T>::value, T>::type
- strConvert(const string& str)
- {
- T res;
- stringstream ss(str);
- ss >> res;
- return res;
- }
上述代码的含义是:若T类型为enum,生成第一个函数,我们将enum视为int来处理;否则,生成第二个函数。
对于其余可能存在的特殊情况,我们也可以采用这种方法实现,此时,我们改进了setStrValue的实现:
- template<typename T>
- class Param
- {
- public:
- // ...
- void setStrValue(const string& strValue) override
- {
- *m_value = strConvert<T>(strValue);
- }
- };
绑定回调函数
在有些情况下,在类中声明变量,我们希望为其赋值初值;又有些情况下,我们希望自定义赋值的过程,或者说,在赋值的同时完成一些别的操作。比如当我们读入一个图像的名字时,我们希望能够同时加载这张图像,存到另一个变量中。
我们扩展了变量声明的宏,同时也支持设定初始值的写法:
- #define Property_Param(type, name, arg) \
- type name = arg; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper); \
为了完成回调,我们需要传入一个函数,记录在BaseObject中,在加载数据的时候回调。为了实现这一目的,我们扩展宏如下:
- #define Property_Func(type, name, func) \
- type name; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper, \
- [&](type val){func(val);}); \
同时在构造函数中支持函数的传入:
- template<typename T>
- class Param : public BaseObject
- {
- // ...
- public:
- Param(const string& inName, T* inValue, CSerializeHelper& helper)
- : BaseObject(inName), m_value(inValue)
- {
- helper.PushBack(this);
- }
- Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T)> onInit)
- : Param(inName, inValue, helper)
- {
- m_funcOnInit = onInit;
- }
- void setStrValue(const string& strValue) override
- {
- if(m_funcOnInit)
- {
- m_funcOnInit(strConvert<T>(strValue));
- }
- else
- {
- *m_value = strConvert<T>(strValue);
- }
- }
- private:
- // ...
- function<void(T)> m_funcOnInit;
- };
这样,我们就能支持形如void(T)类型的函数回调了。但这可能是不够的,有时候我们还希望传入其它参数,同样地以图片地址为例,我们希望传入一个图像对象,把结果绑定到特定的对象中。我们假定第一个参数始终是T,如果希望支持传入任意类型的函数参数,我们需要使用可变长模板,传入一个模板参数包:
- #define Property_Func(type, name, func, ...) \
- type name; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper, \
- [&](type val){func(val,__VA_ARGS__);}); \
- template<typename T,typename... Args>
- class Param : public BaseObject
- {
- public:
- // ...
- Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
- : Param(inName, inValue, helper)
- {
- m_funcOnInit = onInit;
- }
- private:
- // ...
- function<void(T, Args...)> m_funcOnInit;
- };
此时,我们就可以按照如下的写法,传入一个SetImage的回调函数,同时传入QImage和QLuint类型的参数,在回调函数中完成导入图片,并生成纹理id的操作:
- class PBRObject : public Object
- {
- // ...
- GLuint m_nAlbedo = 0;
- QImage m_imgAlbedo;
-
- Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
-
- void SetImage(const string& name, QImage& image, GLuint& texId);
- };
入口
完成了上述一系列参数后,我们需要设置一个入口来调用以上序列化/反序列化函数。
对于保存而言,我们遍历所有Object对象,为其生成子结点,并交由每个对象自己填充child结点。
- void ObjectInfo::Save(const QString& fileName)
- {
- QFile file(fileName);
- if(!file.open(QFile::WriteOnly|QFile::Truncate))
- return;
-
- QDomDocument doc;
- QDomProcessingInstruction instruction;
- instruction = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
-
- doc.appendChild(instruction);
-
- QDomElement root = doc.createElement("objectlist");
- doc.appendChild(root);
- for(size_t i = 0;i < vecObjs.size(); i++)
- {
- QDomElement child = doc.createElement("object");
- vecObjs[i]->Save(child);
- root.appendChild(child);
- }
- QTextStream out_stream(&file);
- doc.save(out_stream,4);
- file.close();
- }
对于导入而言,我们需要先生成一个对象,然后才能让每个对象读入数据。为了能够生成对象,我们首先从xml中读入类型。此时的类型为字符串格式,这里需要我们实现从字符串构造对象,这是另外一个课题,实现在这篇文章中提及:根据字符串自动构造对应类。
- void ObjectInfo::Load(const QString& fileName)
- {
- QFile file(fileName);
- if(!file.open(QFile::ReadOnly))
- {
- qDebug() << "fail open";
- return;
- }
-
- QString errorStr;
- int errorLine;
- int errorColumn;
-
- QDomDocument doc;
- if (!doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
- {
- qDebug() << "Error: Parse error at line " << errorLine << ", "
- << "column " << errorColumn;
- return;
- }
- QDomElement root = doc.documentElement();
- if (root.tagName() != "objectlist")
- {
- qDebug() << "failed load object list";
- return;
- }
-
- QDomNode child = root.firstChild();
- while (!child.isNull())
- {
- QDomElement element = child.toElement();
- if (element.tagName() == "object")
- {
- QString type = element.attribute("m_strType");
-
- if(!type.isEmpty())
- {
- shared_ptr<Object> obj = CreateObject(type.toStdString());
- if(obj)
- {
- obj->Load(element);
- }
- }
- }
- child = child.nextSibling();
- }
- }
.h
- #ifndef OBJECTPROPERTY_H
- #define OBJECTPROPERTY_H
-
- #include <QtXml/QDomDocument>
- #include <QtXml/QDomElement>
- #include <sstream>
- #include <list>
- #include <memory>
- #include <functional>
- using namespace std;
-
- template<typename T>
- typename enable_if<is_enum<T>::value, T>::type
- strConvert(const string& str)
- {
- int res = strConvert<int>(str);
- return static_cast<T>(res);
- }
- template<typename T>
- typename enable_if<!is_enum<T>::value, T>::type
- strConvert(const string& str)
- {
- T res;
- stringstream ss(str);
- ss >> res;
- return res;
- }
-
- class CSerializeHelper
- {
- public:
- class BaseObject
- {
- public:
- BaseObject(const string& inName)
- : m_strName(inName) { }
-
- virtual string getStrName() final { return m_strName; }
- virtual string getStrValue() = 0;
- virtual void setStrValue(const string& strValue) = 0;
- virtual ~BaseObject() = 0;
- private:
- string m_strName;
- };
-
- template<typename T,typename... Args>
- class Param : public BaseObject
- {
- public:
- Param(const string& inName, T* inValue, CSerializeHelper& helper)
- : BaseObject(inName), m_value(inValue)
- {
- helper.PushBack(this);
- }
- Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
- : Param(inName, inValue, helper)
- {
- m_funcOnInit = onInit;
- }
- string getStrValue() override
- {
- if(m_value)
- {
- stringstream ss;
- ss << *(m_value);
- return ss.str();
- }
- return string();
- }
-
- void setStrValue(const string& strValue) override
- {
- if(m_funcOnInit)
- {
- m_funcOnInit(strConvert<T>(strValue));
- }
- else
- {
- *m_value = strConvert<T>(strValue);
- }
- }
-
- ~Param() override { }
- private:
- unique_ptr<T> m_value;
- function<void(T, Args...)> m_funcOnInit;
- };
-
- void PushBack(BaseObject* obj)
- {
- listObjs.push_back(obj);
- }
- void Serialize(QDomElement& child)
- {
- for(BaseObject* obj : listObjs)
- {
- child.setAttribute(QString::fromStdString(obj->getStrName()),
- QString::fromStdString(obj->getStrValue()));
- }
- }
- void Deserialize(QDomElement& child)
- {
- for(BaseObject* obj : listObjs)
- {
- QString attributeName = QString::fromStdString(obj->getStrName());
- if(child.hasAttribute(attributeName))
- {
- QString strValue = child.attribute(attributeName);
- obj->setStrValue(strValue.toStdString());
- }
- }
- }
- private:
- list<BaseObject*> listObjs;
- };
-
-
- #define Property(type, name) \
- type name; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper); \
- #define Property_Param(type, name, arg) \
- type name = arg; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper); \
- #define Property_Func(type, name, func, ...) \
- type name; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper, \
- [&](type val){func(val,__VA_ARGS__);}); \
- #define Property_Param_Func(type, name, arg, func, ...) \
- type name = arg; \
- CSerializeHelper::Param<type>* name##Param \
- = new CSerializeHelper::Param<type> \
- (#name, &name, serializeHelper, \
- [&](type val){func(val,__VA_ARGS__);}); \
-
- #define REGISTER_SERIALIZE \
- CSerializeHelper serializeHelper; \
- void Save(QDomElement& child) \
- { \
- serializeHelper.Serialize(child); \
- } \
- void Load(QDomElement& child) \
- { \
- serializeHelper.Deserialize(child); \
- } \
- #endif // OBJECTPROPERTY_H
cpp:
- string CSerializeHelper::BaseObject::getStrValue() { return string(); }
- CSerializeHelper::BaseObject::~BaseObject() { }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。