Qt属性与反射实现多个不同类型对象的保存与加载

本文针对如下需求场景,提出一种解决方案。

一、需求场景

我们的需求如下:

  • 我们程序需要运行多个算法,每个算法执行不同的处理,并且算法有各自的参数,这些参数也是不同的。

  • 同时提供界面,可以对需要运行的算法进行选择,每个算法的参数值可以编辑。选择好的算法,以及编辑好的参数需要保存为配置文件,下次启动时,自动载入至界面。

二、设计实现

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,即可下载。

在这里插入图片描述

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百里杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值