Как создать общую модель на основе QVariant?

Довольно часто я обнаруживаю, что нуждаюсь в какой-то пользовательской модели схемы, предписывающей реализацию все большего количества моделей, что еще более утомительно из-за невозможности 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 });

person dtech    schedule 10.12.2020    source источник


Ответы (1)


И вот оно.

Обратите внимание, что вы должны быть ответственны за параметры, так как нет безопасности типов, фактически он имеет неявный аналог ListModel dynamicRoles, то есть он будет принимать и работать с любым QVariant совместимым значением в каждом слоте роли.

Что касается эффективности памяти, учтите, что QVariant имеет 8 байтов для данных, плюс 4 байта для идентификатора типа, плюс еще 4 байта заполнения, всего 16 байтов. Это немаловажно, если вы используете его для небольших типов данных, например, bool, поэтому, если у вас есть схема данных с множеством небольших (1–4 байта) полей и множеством элементов, реализация полной модели будет все равно лучший вариант. Это все еще намного лучше, чем общая объектная модель, которую я использую, которая должна нести раздувание из QObject и еще более значительным в случае объектов qml.

Кроме того, поскольку QVariant составляет 16 байт, я решил не использовать удобство QVariantList для хранения данных, в основе которого лежит QList, что усугубило ситуацию. Хотя это исправлено в Qt 6, который избавляется от QList как есть и заменяет его псевдонимом QVector. Тем не менее, std::vector помогает избежать этого в любом случае, плюс на самом деле он может быть немного быстрее, поскольку ему не приходится иметь дело с COW и атомарными счетчиками ссылок. Есть несколько вспомогательных методов, помогающих с предварительным выделением и освобождением памяти.

Модель имеет защиту от изменения ролей по понятным причинам, последняя в первую очередь предназначена для однократной инициализации, но есть reset(), предназначенный для использования в более динамичном контексте qml, что позволяет переопределить схему модели. на лету и предоставить совместимый делегат. Ради определенности роли могут быть переопределены только после явного сброса модели.

Существует небольшая разница во вставке: на стороне c++ пакет параметров передается в оболочке {}, в qml он обертывается в [], причем в обоих случаях используется неявное преобразование в зависимости от контекста. Также обратите внимание, что qml в настоящее время не поддерживает пропуск параметров со значениями по умолчанию, предоставленными на стороне c++, поэтому для добавления вам необходимо указать недопустимый индекс. Естественно, было бы тривиально добавить удобные методы для добавления и добавления в начале, если это необходимо.

В дополнение к примеру синтаксиса вопроса также можно добавить несколько элементов одновременно из декларативной структуры 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 и ожидает возврата логического значения, функтор C++ можно легко добавить через std::function, если это необходимо. Также обратите внимание, что ему нужен указатель на фактический движок 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
  }
person dtech    schedule 10.12.2020
comment
Хорошая работа! Мне нравится, как похоже выглядит интерфейс в QML и C++. Будет ли это похоже на QQMLVariantListModel? - person JarMan; 11.12.2020
comment
@JarMan Не совсем так, если вы посмотрите на его код, он предоставляет только одну роль qtVariant, это всего лишь одна запись данных для каждого элемента модели. Моя реализация поддерживает произвольное количество описательно названных ролей. - person dtech; 11.12.2020
comment
Что касается разрешения роли из строки, это не должно быть проблемой, если у вас нет множества ролей, что было бы довольно странным вариантом использования, хотя модель полностью поддерживает его. Линейный поиск по-прежнему остается самым быстрым способом найти что-то в коллекции, состоящей из дюжины элементов или около того, и достаточно быстрым даже в 5 раз больше. - person dtech; 12.12.2020