QMap与对象互转的思考

2023-03-17 15:20:25 浏览数 (2)

本文通过代码演示SkinConfig对象与QVariantMap怎么方便地转换的思考过程。

代码语言:javascript复制
struct SkinConfig
{
    int    width   = 0;
    int    height  = 0;
    bool   visible = false;
    float  opacity = 1.0;
    QColor color   = Qt::white;

    static SkinConfig fromMap(const QVariantMap &map);
    QVariantMap toMap() const;
}

QVariantMapSkinConfig使用的是fromMap接口,而SkinConfigQVariantMap使用的是toMap接口。   我们看看他们的实现:

代码语言:javascript复制
static SkinConfig fromMap(const QVariantMap &map)
{
    SkinConfig config;
    config.width   = map.value("width",  0).toInt();
    config.height  = map.value("height", 0).toInt();
    config.opacity = map.value("opacity", false).toFloat();
    config.visible = map.value("visible", false).toBool();
    config.color   = map.value("color", QColor(Qt::white)).value<QColor>();

    return config;
}

QVariantMap toMap() const
{
    QVariantMap map;
    map["width"]   = width;
    map["height"]  = height;
    map["opacity"] = opacity;
    map["visible"] = visible;
    map["color"]   = color;

    return map;
}

  上面代码,我们会发现,由于使用字符串作为key值字段,容易写错,且fromMaptoMap的字段都需要一致,不然就会转换不完整。还有就是字符串写错了,编译器也不会报错,会增加维护代码的工作量。

  既然需要字段一致还要编译器提示,那么我们就试试用宏来实现下吧。大致思路是将widthheightopacity等这些成员变量用#变量名字转换为字符串。比如:

代码语言:javascript复制
#define TO_STRING(var) (#var) // 将传递的var替换为字符串"var"

static SkinConfig fromMap(const QVariantMap &map)
{
    SkinConfig config;
    config.width   = map.value(TO_STRING(width),  0).toInt();
    config.height  = map.value(TO_STRING(height), 0).toInt();
    ...
    return config;
}

QVariantMap toMap() const
{
    QVariantMap map;
    map[TO_STRING(width)]   = width;
    map[TO_STRING(height)]  = height;
    ...
    return map;
}

  上面代码中,我们可以看到使用宏TO_STRING很方便地将变量转为字符串了,还能受到编译器的语法检查,一举两得啊。

  用在实际项目中,用得不太顺手啊,一堆的TO_STRING字符串,默认值设置和fromMaptoInt()toBool()转换,这样做太啰嗦了,君君心里想,还是再改改吧。

  一步一步来,貌似默认值设置可以从一个空的构造类成员获取,那就写成这样吧。

代码语言:javascript复制
static SkinConfig fromMap(const QVariantMap &map)
{
    SkinConfig config;
    config.width   = map.value(TO_STRING(width),  SKinConfig().width).toInt();
    config.height  = map.value(TO_STRING(height), SkinConfig().height).toInt();
    ...
    return config;
}

  因为构造的时候已经初始化变量了,所以这就是为什么构造的时候初始化变量的好处了,这里可以让变量的构造初始化和QMap的value接口的默认值传入一致。可是,,,代码更啰嗦了,呜呜,不行,改改改!

  想了想,还是用宏替换吧。于是代码就变成这样了:

代码语言:javascript复制
#define MAP_TO_OBJECT_ITEM(map, obj, key) 
      obj.key = map.value(#key, decltype(obj)().key)

#define OBJECT_TO_MAP_ITEM(obj, map, key) 
      map[#key] = (obj).key

static SkinConfig fromMap(const QVariantMap &map)
{
    SkinConfig config;
    MAP_TO_OBJECT_ITEM(map, config, width).toInt();
    MAP_TO_OBJECT_ITEM(map, config, height).toInt();
    ...
 return config
}

QVariantMap toMap() const
{
    QVariantMap map;
    OBJECT_TO_MAP_ITEM1(*this, map, width);
    OBJECT_TO_MAP_ITEM1(*this, map, height);
    ...
    return map;
}

  代码简短了一点点,可是MAP_TO_OBJECT_ITEM处理后还要自己手动转换数据,这个就很难看了,toMap*this多了个*传递,也太啰嗦了,改改改。

代码语言:javascript复制
template <typename T> T &point2Ref(T &t) { return  t; }
template <typename T> T &point2Ref(T *t) { assert(t != nullptr); return *t; }

#define MAP_TO_OBJECT_ITEM(map, obj, key) 
    point2Ref(obj).key = 
    point2Ref(map).value(#key, std::remove_pointer<decltype(obj)>::type().key) 
                  .value<decltype(point2Ref(obj).key)>();

#define OBJECT_TO_MAP_ITEM(obj, map, key) 
    point2Ref(map)[#key] = point2Ref(obj).key;

static SkinConfig fromMap(const QVariantMap &map)
{
    SkinConfig config;
    MAP_TO_OBJECT_ITEM(map, config, width);
    MAP_TO_OBJECT_ITEM(map, config, height);
    MAP_TO_OBJECT_ITEM(map, config, opacity);
    MAP_TO_OBJECT_ITEM(map, config, visible);
    MAP_TO_OBJECT_ITEM(map, config, color);

    return config;
}

QVariantMap toMap() const
{
    QVariantMap map;
    OBJECT_TO_MAP_ITEM(this, map, width);
    OBJECT_TO_MAP_ITEM(this, map, height);
    OBJECT_TO_MAP_ITEM(this, map, opacity);
    OBJECT_TO_MAP_ITEM(this, map, visible);
    OBJECT_TO_MAP_ITEM(this, map, color);

    return map;
}

  上面代码中使用到了模板和宏,不得不说,宏和模板太适合实现奇淫技巧的操作了。简单介绍下代码的实现。

  • 模板point2Ref的作用是将指针转换为引用,其实现是使用模板特化的原理。将指针转为引用,就可以统一使用.去获取成员变量,而不用区分是指针就用->,非指针就用.
  • decltype(obj)获取对象的类型,比如:
代码语言:javascript复制
int a = 0;
decltype(a) => int

int *b = 0;
decltype(b) => int*
  • std::remove_pointer是移除指针的类型,比如:
代码语言:javascript复制
int *a = 0;
std::remove_pointer(a) => int
  • std::remove_pointer<decltype(obj)>::type()就是获取传入对象的默认构造的值对象。比如:
代码语言:javascript复制
SkinConfig config;
std::remove_pointer<decltype(config)>::type() => SkinConfig()

SkinConfig config = new SkinConfig();
std::remove_pointer<decltype(config)>::type() => SkinConfig()

最后

MAP_TO_OBJECT_ITEMOBJECT_TO_MAP_ITEM传入的mapobj无论是指针还是值都能正确识别,大大提升编码效率。当然缺点还是有的,就是字段名字与函数命名绑定了,如果他们不一致就没法用该方法了。

  如需保存到配置文件持久化,可以将QVariantMap转为字符串再保存为文件:

代码语言:javascript复制
QByteArray data = QJsonDocument::fromVariant(map).toJson();

  字符串=>map=>对象:

代码语言:javascript复制
QVariantMap map = QJsonDocument::fromJson(data).toVariant().toMap();
skinConfig = SkinConfig::fromMap(map);

  大家如果有更好的实现或想法,请留言评论吧。

  源码地址:https://github.com/aeagean/QMap2Object

0 人点赞