【问题标题】:How to create a QVariant-based generic model?如何创建基于 QVariant 的通用模型?
【发布时间】:2020-12-10 21:04:38
【问题描述】:

我经常发现自己需要一些自定义方案模型,要求实施越来越多的模型,而由于无法将 QObject 派生类作为模板而变得更加乏味。

Qt 有 QStandardItemModel,但这似乎有点冗长且使用不便,尤其是从 qml 方面来看,而且对于基本列表模型来说完全是多余的。

还有基本的 qml ListModel,但在 C++ 端使用是有限且不优雅的,我确实怀疑它比它需要的更臃肿。

Qt 有QVariant,这是它的模型/视图架构在内部使用的,所以令人惊讶的是该框架没有提供如下简单的东西:

// qml code
  VarMod {
    roles: ["name", "age", "weight"]
    Component.onCompleted: {
      insert(["Jack", 34, 88.5], -1) // qml doesn't support
      insert(["Mary", 26, 55.3], -1) // default arg values
    }
  }

// cpp code
VarMod vm { "name", "age", "weight" }; // member declaration
vm.insert({ "Jack", 34, 88.5 });
vm.insert({ "Mary", 26, 55.3 });

【问题讨论】:

    标签: qt qml qtquick2 qvariant qabstractlistmodel


    【解决方案1】:

    就在这里。

    请注意,您必须对参数负责,因为没有类型安全,实际上它与ListModeldynamicRoles 具有隐式模拟 - 也就是说,它将接受并使用任何@987654327 @ 每个角色槽上的兼容值。

    至于内存效率,考虑QVariant有8个字节的数据,加上4个字节的类型id,再加上4个字节的填充,一共16个字节。如果您将它用于小型数据类型,例如bool,那么这并不是微不足道的,因此如果您有一个包含许多小(1 - 4 字节)字段和大量项目的数据方案,则可以实现完整的模型仍然是更好的选择。它仍然比我使用的generic object model 好很多,它必须是QObjectcarry the bloat,在qml 对象的情况下是even more significant

    此外,QVariant 是 16 字节,我选择不使用 QVariantList 的便利性进行数据存储,它具有底层 QList,这使情况变得比需要的更糟。虽然这在 Qt 6 中得到了修复,它删除了 QList,并将其替换为别名 QVector。尽管如此,std::vector 在任何情况下都有助于避免这种情况,而且它实际上可能会快一点,因为它不必处理 COW 和原子引用计数器。还有几种辅助方法可以帮助预分配和释放内存。

    该模型可以防止由于明显原因而更改角色,后者主要旨在仅初始化一次,但reset()旨在用于更动态的qml上下文,使其成为可能动态重新定义模型模式并提供兼容的委托。为确定起见,角色只能在模型被显式重置后重新定义。

    在插入方面存在细微差别,在 c++ 端,参数包以{} 包装传递,在 qml 中它以[] 包装,两者都以特定于上下文的方式利用隐式转换。另外,请注意,qml 目前不支持在 c++ 端使用默认值省略参数,因此对于追加,您必须提供无效索引。当然,如果需要,添加用于追加和前置的便捷方法将是微不足道的。

    除了问题的语法示例,还可以一次添加多个项目,来自“declarative-y”qml结构如:

    let v = [["Jack", 34, 88.5],
             ["Mary", 26, 55.3],
             ["Sue", 22, 69.6]]
    vm.insertList(v, -1)
    

    最后,如果场景确实需要类型安全,则可以实现类型安全,然后可以为每个角色指定预期的类型以配合它,例如:

    VarMod vm {{"name", QMetaType::QString},
               {"age", QMetaType::Int},
               {"weight", QMetaType::QReal}};
    

    然后迭代并进行必要的检查以确保插入时的类型安全。

    更新:我还添加了序列化,以及从磁盘保存/加载功能,请注意,这会将数据与模式架构一起序列化。

    class VarMod : public QAbstractListModel {
        Q_OBJECT
        Q_PROPERTY(QVariantList roles READ roles WRITE setRoles NOTIFY rolesChanged)
        QVariantList vroles;
        QVariantList roles() const { return vroles; }
        QHash<int, QByteArray> _roles;
        std::vector<std::vector<QVariant>> _data;
        inline bool checkArgs(int rc) const {
          if (rc == _roles.size()) return true;
          qWarning() << "arg size mismatch, got / expected" << rc << _roles.size();
          return false;
        }
        inline bool inBounds(int i, bool ok = false) const {
          if (i > -1 && i < (int)_data.size()) return true;
          if (!ok) qWarning() << "out of bounds" << i; // do not warn if intentionally appending
          return false;
        }
        inline bool validRole(int r) const { return (r > -1 && r < _roles.size()); }
      protected:
        QHash<int, QByteArray> roleNames() const override { return _roles; }
        int rowCount(const QModelIndex &) const override { return _data.size(); }
        QVariant data(const QModelIndex &index, int r) const override {
          r = r - Qt::UserRole - 1;
          if (inBounds(index.row()) && validRole(r)) return _data[index.row()][r];
          return QVariant();
        }
      public:
        VarMod() {} // for qml
        VarMod(std::initializer_list<QByteArray> r) {
          int rc = Qt::UserRole + 1;
          for (const auto & ri : r) {
            _roles.insert(rc++, ri);
            vroles << QString::fromLatin1(ri);
          }
          rolesChanged();
        }
        inline void insert(std::initializer_list<QVariant> s, int i = -1) {
          if (!checkArgs(s.size())) return;
          insert(QVariantList(s), i);
        }
        inline bool setItem(int i, std::initializer_list<QVariant> s) {
          if (checkArgs(s.size())) return setItem(i, QVariantList(s));
          return false;
        }
        void setRoles(QVariantList r) {
          if (_roles.empty()) {
            int rc = Qt::UserRole + 1;
            for (const auto & vi : r) _roles.insert(rc++, vi.toByteArray());
            vroles = r;
            rolesChanged();
          } else qWarning() << "roles are already initialized";
        }
        void read(QDataStream & d) {
          reset();
          QVariantList vr;
          d >> vr;
          quint32 s;
          d >> s;
          _data.resize(s);
          for (uint i = 0; i < s; ++i) {
            _data[i].reserve(vr.size());
            for (int c = 0; c < vr.size(); ++c) {
              QVariant var;
              d >> var;
              _data[i].push_back(std::move(var));
            }
          }
          setRoles(vr);
          beginResetModel();
          endResetModel();
        }
        void write(QDataStream & d) const {
          d << vroles;
          d << (quint32)_data.size();
          for (const auto & v : _data) {
            for (const auto & i : v) d << i;
          }
        }
      public slots:
        void insert(QVariantList s, int i) {
          if (!inBounds(i, true)) i = _data.size();
          if (!checkArgs(s.size())) return;
          beginInsertRows(QModelIndex(), i, i);
          _data.insert(_data.begin() + i, { s.cbegin(), s.cend() });
          endInsertRows();
        }
        void insertList(QVariantList s, int i) {
          if (!inBounds(i, true)) i = _data.size();
          int added = 0;
          for (const auto & il : s) {
            QVariantList ll = il.value<QVariantList>();
            if (checkArgs(ll.size())) {
              _data.insert(_data.begin() + i + added++, { ll.cbegin(), ll.cend() });
            }
          }
          if (added) {
            beginInsertRows(QModelIndex(), i, i + added - 1);
            endInsertRows();
          }
        }
        bool setData(int i, int r, QVariant d) {
          if (!inBounds(i) || !validRole(r)) return false;
          _data[i][r] = d;
          dataChanged(index(i), index(i));
          return true;
        }
        bool setDataStr(int i, QString rs, QVariant d) { // a tad slower
          int r = _roles.key(rs.toLatin1()); // role is resolved in linear time
          if (r) return setData(i, r - Qt::UserRole - 1, d);
          qWarning() << "invalid role" << rs;
          return false;
        }
        bool setItem(int i, QVariantList d) {
          if (!inBounds(i) || !checkArgs(d.size())) return false;
          _data[i] = { d.cbegin(), d.cend() };
          dataChanged(index(i), index(i));
          return true;
        }
        QVariantList item(int i) const {
          if (!inBounds(i)) return QVariantList();
          const auto & v = _data[i];
          return { v.begin(), v.end() };
        }
        QVariant getData(int i, int r) const {
          if (inBounds(i) && validRole(r)) return _data[i][r];
          return QVariant();
        }
        QVariant getDataStr(int i, QString rs) const {
          int r = _roles.key(rs.toLatin1()); // role is resolved in linear time
          if (r) return getData(i, r);
          qWarning() << "invalid role" << rs;
          return QVariant();
        }
        QVariantList take(int i) {
          QVariantList res = item(i);
          if (res.size()) remove(i);
          return res;
        }
        bool swap(int i1, int i2) {
          if (!inBounds(i1) || !inBounds(i2)) return false;
          std::iter_swap(_data.begin() + i1, _data.begin() + i2);
          dataChanged(index(i1), index(i1));
          dataChanged(index(i2), index(i2));
          return true;
        }
        bool remove(int i) {
          if (!inBounds(i)) return false;
          beginRemoveRows(QModelIndex(), i, i);
          _data.erase(_data.begin() + i);
          endRemoveRows();
          return true;
        }
        void clear() {
          beginResetModel();
          _data.clear();
          _data.shrink_to_fit();
          endResetModel();
        }
        void reset() {
          clear();
          _roles.clear();
          vroles.clear();
          rolesChanged();
        }
        void reserve(int c) { _data.reserve(c); }
        int size() const { return _data.size(); }
        int capacity() const { return _data.capacity(); }
        void squeeze() { _data.shrink_to_fit(); }
        bool fromFile(QString path) {
          QFile f(path);
          if (!f.open(QIODevice::ReadOnly)) return false;
          QDataStream d(&f);
          read(d); // assumes correct data
          return true;
        }
        bool toFile(QString path) const {
          QFile f(path);
          if (!f.open(QIODevice::WriteOnly)) return false;
          QDataStream d(&f);
          write(d);
          return true;
        }
      signals:
        void rolesChanged();
    };
    

    我还创建了这个排序/过滤视图来补充模型:

    class View : public QSortFilterProxyModel {
        Q_OBJECT
        Q_PROPERTY(QJSValue filter READ filter WRITE set_filter NOTIFY filterChanged)
        Q_PROPERTY(bool reverse READ reverse WRITE setReverse NOTIFY reverseChanged)
        bool reverse() const { return _reverse; }
        void setReverse(bool v) {
          if (v == _reverse) return;
          _reverse = v;
          reverseChanged();
          sort(0, (Qt::SortOrder)_reverse);
        }
        bool _reverse = false;
        mutable QJSValue m_filter;
        QJSValue & filter() const { return m_filter; }
        void set_filter(QJSValue & f) {
          if (!m_filter.equals(f))
            m_filter = f;
            filterChanged();
            invalidateFilter();
          }
        }
      public:
        View(QObject *parent = 0) : QSortFilterProxyModel(parent) { sort(0, (Qt::SortOrder)_reverse); }
      signals:
        void filterChanged();
        void reverseChanged();
      protected:
        bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override {
          if (!m_filter.isCallable()) return true;
          VarMod * vm = qobject_cast<VarMod *>(sourceModel());
          if (!vm) {
            qWarning() << "model is not varmod";
            return true;
          }
          return m_filter.call({_engine->toScriptValue(vm->item(sourceRow))}).toBool();
        }
        bool lessThan(const QModelIndex &left, const QModelIndex &right) const override {
          VarMod * vm = qobject_cast<VarMod *>(sourceModel());
          if (!vm) {
            qWarning() << "model is not varmod";
            return false;
          }
          return vm->getData(left.row(), sortRole()) < vm->getData(right.row(), sortRole());
        }
    };
    

    对于排序,您只需指定排序角色,注意它是“列”的索引,而不是角色哈希中的 int 值。对于过滤,它通过一个 qml 函子工作,该函子将模型项作为 JS 数组接收,并期望返回一个布尔值,如果需要,可以通过 std::function 轻松添加 c++ 函子。另请注意,它需要一个指向实际 qml 引擎的指针。

      View {
        id: vv
        sourceModel: vm
        sortRole: sr.value
        reverse: rev.checked
        filter: { sa.value; o => o[1] < sa.value } // "capturing" sa.value to react to value changes
      }
    

    【讨论】:

    • 干得好!我喜欢这个界面在 QML 和 C++ 中的相似度。这会类似于QQMLVariantListModel吗?
    • @JarMan 不是真的,如果你看一下它的代码,它只提供一个qtVariant 角色,这只是每个模型项的一个数据条目。我的实现支持任意数量的描述性命名角色。
    • 至于从字符串中解析角色,除非您有许多角色,否则这应该不是问题,尽管模型完全支持这将是一个非常奇怪的用例。线性搜索仍然是在多达十几个元素的集合中查找内容的最快方法,而且速度足够快,甚至可以快 5 倍。
    猜你喜欢
    • 2011-08-02
    • 1970-01-01
    • 1970-01-01
    • 2020-03-29
    • 2016-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-31
    相关资源
    最近更新 更多