本文针对如下需求场景,提出一种解决方案。
一、需求场景
我们的需求如下:
-
我们程序需要运行多个算法,每个算法执行不同的处理,并且算法有各自的参数,这些参数也是不同的。
-
同时提供界面,可以对需要运行的算法进行选择,每个算法的参数值可以编辑。选择好的算法,以及编辑好的参数需要保存为配置文件,下次启动时,自动载入至界面。
二、设计实现
1、解决第1条需求
首先,因为多个算法需要被运行,我们很容易想到抽象出一个IAlgo接口类,如下:
class IAlgo
{
public:
virtual void run() = 0;
};
然后,由IAlgo派生出不同的算法,在子类中实现具体的算法逻辑,并定义参数为成员变量,如下:
class Algo1 : public IAlgo
{
public:
Algo1();
~Algo1();
virtual void run() override
{
// ...
}
private:
int param1_1;
QString param1_2;
bool param1_3;
};
class Algo2 : public IAlgo
{
public:
Algo2();
~Algo2();
virtual void run() override
{
// ...
}
private:
int param2_1;
int param2_2;
bool param2_3;
float param2_4;
};
到此,第1条需求似乎已经解决。
2、解决第2条需求
下面看第2条,关键信息是:
- 多个算法保存为配置文件;
- 从配置文件读取多个算法,用于将选择的算法信息显示到界面;
- 从配置文件读取多个算法,用于程序执行算法。
按照上述的设计,每个算法是一个class,要将多个算法保存为配置文件,那么就是需要把不同class的对象,其内部成员变量进行保存。
在传统C++中,实现这一点还是比较麻烦的,接口只能统一各个类的函数访问,无法统一各个类的成员变量访问。
我们提出两个问题:
- 怎么实现以统一的方式保存不同算法对象的成员变量?
- 根据从配置文件中读取的算法成员变量,怎么还原算法对象?
我们可以使用Qt的属性系统,结合反射来解决这2个问题。
(1)算法类
下面需要对Algo1、Algo2进行修改,将成员变量定义为属性。如下:
class Algo1 : public IAlgo
{
Q_OBJECT
Q_PROPERTY(int size READ getSize WRITE setSize)
Q_PROPERTY(QString path READ getPath WRITE setPath)
Q_PROPERTY(bool shared READ getShared WRITE setShared)
public:
Q_INVOKABLE Algo1();
~Algo1();
virtual void run() override
{
qDebug() << "Algo1::run()" << "size=" << size << ",path=" << path << ",shared=" << shared;
}
int getSize() const;
void setSize(int value);
QString getPath() const;
void setPath(const QString &value);
bool getShared() const;
void setShared(bool value);
private:
int size;
QString path;
bool shared;
};
class Algo2 : public IAlgo
{
Q_OBJECT
Q_PROPERTY(int width READ getWidth WRITE setWidth)
Q_PROPERTY(int height READ getHeight WRITE setHeight)
Q_PROPERTY(bool shared READ getShared WRITE setShared)
Q_PROPERTY(float pi READ getPi WRITE setPi)
public:
Q_INVOKABLE Algo2();
~Algo2();
virtual void run()
{
qDebug() << "Algo2::run()" << "width=" << width << ",height=" << height << ",shared=" << shared << ",pi=" << pi;
}
int getWidth() const;
void setWidth(int value);
int getHeight() const;
void setHeight(int value);
bool getShared() const;
void setShared(bool value);
float getPi() const;
void setPi(float value);
private:
int width;
int height;
bool shared;
float pi;
};
关于Qt属性的定义,请自行了解,或参考《Qt属性系统详细使用教程》。
另外,需要在构造函数前加
Q_INVOKABLE
以确保后面使用反射时,可以调用该构造函数进行实例化。
接下来,我们在基类IAlgo中,添加获取当前对象所有属性的函数,以及设置所有属性的函数,如下:
class IAlgo : public QObject
{
Q_OBJECT
public:
IAlgo() {}
QVariantMap getProperties()
{
QVariantMap properties;
const QMetaObject *metaobject = metaObject();
int count = metaobject->propertyCount();
for (int i = 0; i < count; ++i)
{
QMetaProperty metaproperty = metaobject->property(i);
const char *name = metaproperty.name();
QVariant value = property(name);
properties[name] = value;
}
return properties;
}
void setProperties(const QVariantMap &properties)
{
QStringList names = properties.keys();
foreach (auto name, names)
{
QVariant value = properties.value(name);
setProperty(name.toStdString().c_str(), value);
}
}
virtual void run() = 0;
};
getProperties()将当前对象成员变量名-值打包成map,返回,用于统一保存到配置文件。
setProperties()用传入的成员变量名-值map,对当前对象进行成员变量赋值。
(2)ini配置读写类
接下来,我们创建ini配置读写类,如下:
IniConfig::IniConfig()
{
setting = new QSettings(qApp->applicationDirPath() + "/setting.ini", QSettings::IniFormat);
}
IniConfig::~IniConfig()
{
delete setting;
setting = nullptr;
}
void IniConfig::setAlgos(const QList<IAlgo*>& algos)
{
QVariantList algos_properties;
foreach (auto algo, algos)
{
QVariantMap properties = algo->getProperties();
const char *className = algo->metaObject()->className();
properties["className"] = className;
algos_properties.append(properties);
}
setting->setValue("app/algos", algos_properties);
}
QList<IAlgo *> IniConfig::getAlgos() const
{
QList<IAlgo *> algos;
QVariantList algos_properties = setting->value("app/algos").toList();
foreach (auto properties, algos_properties)
{
QVariantMap map = properties.toMap();
QByteArray className = map.value("className").toByteArray();
IAlgo* algo = qobject_cast<IAlgo*>(Reflect::newInstance(className));
if (algo == nullptr)
{
qDebug() << className << "class name is not registered!";
continue;
}
map.remove("className");
algo->setProperties(map);
algos.append(algo);
}
return algos;
}
setAlgos(const QList<IAlgo*>& algos),对每个算法获取属性,并添加"className"属性,然后将所有算法属性进行保存。
getAlgos(),对读取的每个算法数据,获取"className"属性,并进行反射创建算法实例,将所有算法实例返回。
(3)反射模板类
下面给出ini配置类中使用到的反射模板类,如下:
class Reflect
{
public:
template<typename T>
static void registerClass()
{
metaObjects().insert( T::staticMetaObject.className(), T::staticMetaObject );
}
static QObject* newInstance( const QByteArray& className,
QGenericArgument val0 = QGenericArgument(nullptr),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument() )
{
Q_ASSERT( metaObjects().contains(className) );
return metaObjects().value(className).newInstance(val0, val1, val2, val3, val4,
val5, val6, val7, val8, val9);
}
private:
static QHash<QByteArray, QMetaObject>& metaObjects()
{
static QHash<QByteArray, QMetaObject> instance;
return instance;
}
};
实现方式比较简单,有兴趣,大家自行查看。也可以当成个轮子来用。
使用前提:
- 必须继承自QObject;
- 必须添加Q_OBJECT宏;
- 必须对构造函数添加Q_INVOKABLE宏。
使用方式:
必须先注册registerClass(),后反射newInstance()。
3、测试代码
测试代码,如下:
void writeToConfigFile()
{
Algo1 algo1;
algo1.setSize(10);
algo1.setPath("123");
algo1.setShared(true);
Algo2 algo2;
algo2.setWidth(20);
algo2.setHeight(30);
algo2.setShared(false);
algo2.setPi(3.14);
QList<IAlgo*> list;
list.append(&algo1);
list.append(&algo2);
IniConfig config;
config.setAlgos(list);
}
void readFromConigFile()
{
IniConfig config;
QList<IAlgo *> list = config.getAlgos();
foreach (auto algo, list)
{
algo->run();
}
qDeleteAll(list);
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 注册类型
Reflect::registerClass<Algo1>();
Reflect::registerClass<Algo2>();
writeToConfigFile();
readFromConigFile();
return a.exec();
}
分别测试了Algo1、Algo2存ini文件,以及从ini文件读取并实例化Algo1、Algo2,并调用run()方法。
运行效果,如下:
三、总结
1、实现方案与序列化方案的对比
我们实现的功能,是将多个不同对象进行保存,并进行恢复。
从表面上看,这与对象的序列化与反序列化,很像,但实际却不一样。
序列化与反序列化,要求写入对象的顺序与读取顺序完全一致,编写代码的人对顺序应该是明确且清楚的,如下:
Algo1 algo1;
Algo2 algo2;
QDataStream out;
out << algo1 << algo2;
QDataStream in;
in >> algo1 >> algo2;
in >> algo2 >> algo1; // is error
然鹅,我们从配置读取算法时,其顺序取决于用户对算法的选择,故是没有顺序的,编写代码的人也不清楚顺序。
所以,对于这个需求,使用序列化方案是无法实现的。
2、对象保存形式的扩展
我们这里将对象保存到了ini文件中,作为一个键值。
其实也可以,保存到json、xml中,就像QtDesigner生成的界面ui文件一样,如下:
将对象保存到xml,对象下面还可以嵌套子对象,子子对象等;
基于Qt属性和反射,那么就可以实现QtDesigner这样的功能了:
- 导出界面xml(对象保存);
- 导入界面xml,生成界面控件(对象恢复)。
若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!
同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“超哥学编程”,即可领取。
本文涉及工程代码,公众号回复:51ObjectSerialization,即可下载。