首发于 异想天开

Qt - 模仿Vue搭建视图模型

上一篇 快速开发MVC框架 的文章里面,需要一个视图模型去搭建数据中心。

虽然Qt本身也有自带视图模型结构,也是一种数据映射,但是非常难用。

想要创建一个简单的图表,就需要先定义QAbstractItemModel,再定义需要显示的QTabelView,最后还要通过delegate代理去过滤和交互。

上面这一套写下来就很麻烦,后期需要添加新的数据或者视图,又会洋洋洒洒写一堆。

我曾经就维护一套视图模型的项目,每天加班加点就是在各个.cpp中间跳来跳去,工作效率极低。

所有每次看到别的前端语言有好用的视图绑定那一套,我都很想直接抄袭搬用到Qt上面。




之前我参考过React里面的通过界面控件的值绑定state,在通过界面交互触发setState钩子函数,修改state值再重新渲染界面。

但是有一个问题,React会通过state值去构建一颗虚拟树,setState只是修改虚拟树上面的值。

真实的渲染是React内部一直在循环比较虚拟树和真实界面树差异,一旦有差异才会完成一次界面更新,他这一套更新机制很难仿到精髓。

在Qt上面去实现的话,就是定义一组state数据集,添加一个线程以固定频率刷新界面控件上面的值。

怎么快速比较真实数据和state数据集

怎么能在state数据集快速索引指定值的速度

怎么能在state加锁情况下刷新界面取值不卡帧

能力有限,以上的种种复杂的优化机制使我选择放弃选择模仿React来做视图模型。




最近在学Vue发现一个很好用的特性,界面数据映射到内部Data数据,通过简单双向绑定完成数据响应式。

根据Vue这个思路想到Qt身上,不就是定义自带信号槽的Data值,一头绑定去界面控件的值,一头绑定到业务层的使用。

如果Data值需要做到通用性,那数据类型肯定不能固定,一开始我想用QVariant做Data的类型。

但是在频率高的场合下,QVariant转化就显得效率低,后来就想到直接用模板来代替数据类型。

结果遇到新问题,由于Qt的需要moc预编译,直接定义模板和预编译冲突了。

QT : Templated Q_OBJECT class

只好把需要信号发送的部分单独封装,封装成DataObject里面。

DataBase就是自带信号的类,通过getData获取值,通过setDataByModel去设置参数。

通过模板自动去判断DataObject重载的信号类型,这样就可以规避掉moc之间的冲突。

我把信号定义写成一个简单的宏定义,通过NEW_SIGNAL去注册数据类型,对于复杂参数用Q_DECLARE_METATYPE声明一下

#define DATABASE_H
#include <QObject>
#include <QVariant>
#include <QVector>
#include <QSet>
#include <QVector2D>
#include <QVector3D>
Q_DECLARE_METATYPE(QVector<QVector2D>)
Q_DECLARE_METATYPE(QVector<QVector3D>)
#ifndef NEW_SIGNAL
#define NEW_SIGNAL(Type) \
    void changedToModel(const Type &data); \
    void changedToView(const Type &data);
class DataObject : public QObject
    Q_OBJECT
signals:
    NEW_SIGNAL(int)
    NEW_SIGNAL(float)
    NEW_SIGNAL(double)
    NEW_SIGNAL(QSet<int>)
    NEW_SIGNAL(QVector<float>)
    NEW_SIGNAL(QVector<double>)
    NEW_SIGNAL(QVector<QVector2D>)
    NEW_SIGNAL(QVector<QVector3D>)
template <typename Type>
class DataBase
public:
    DataBase() {}
    DataBase(Type data) : m_data(data) {}
public:
    const Type &getData(){return m_data;}
    void setDataByModel(const Type &data)
        m_data = data;
        m_object.changedToView(data);
    void setDataByView(const Type &data)
        m_data = data;
        m_object.changedToModel(data);
    DataObject m_object;
protected:
    Type m_data;
#undef NEW_SIGNAL
#endif
#ifndef NEW_FUNC
#define NEW_FUNC(Name, Type, Value) \
public: \
    static DataBase<Type>* Name() { return DataManager::instance()->m_##Name; } \
    static DataObject* Name##Object() { return &DataManager::instance()->m_##Name->m_object; } \
    const Type &Name##Data() { return m_##Name->getData(); } \
protected: \
    DataBase<Type> *m_##Name = new DataBase<Type>(Value);
class DataManager
public:
    static DataManager* instance()
        static DataManager dataCenter;
        return &dataCenter;
    NEW_FUNC(dataBool, bool, false)
    NEW_FUNC(dataInt, int, 0)
    NEW_FUNC(dataIntArray, QSet<int>, QSet<int>())
    NEW_FUNC(dataFloatArray, QVector<float>, QVector<float>())
    NEW_FUNC(data2DArray, QVector<QVector2D>, QVector<QVector2D>())
    NEW_FUNC(data3DArray, QVector<QVector3D>, QVector<QVector3D>())
#undef NEW_FUNC
#endif
#endif // DATABASE_H

基本上可以用几百行不到的代码就可以定义这套视图结构,把DataManager定义对外接口。

通过NEW_FUNC去定义数据和调用接口,这个写法我是参考了Q_OBJECT,直接用宏定义来快捷生成函数代码。

在视图端的调用就更简单

    auto spinBox = new QSpinBox();
    connect(DataManager::dataIntObject(), SIGNAL(changedToView(const int &)), spinBox, SLOT(setValue(int)));
    connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), [](int value){
        DataManager::dataIntIndex()->setDataByView(value);

就等同于<spinBox value={{ dataInt }}/>

NEW_FUNC(dataBool, bool, false)
NEW_FUNC(dataInt, int, 0)

就等同于var vm=new Vue({ data:{ dataBool : false, dataInt : 0 } })


而在模型端调用也很简单

    connect(DataManager::dataBoolDataObject(), QOverload<const bool &>::of(&DataObject::changedToModel), 
            [](const bool &data) {
        //to doing tring