จะสร้างโมเดลทั่วไปที่ใช้ 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's dynamicRoles ซึ่งหมายความว่าจะยอมรับและทำงานกับค่าที่เข้ากันได้กับ QVariant ใดๆ ในทุกช่องบทบาท

สำหรับประสิทธิภาพของหน่วยความจำ ให้พิจารณาว่า QVariant มีข้อมูล 8 ไบต์ บวก 4 ไบต์สำหรับประเภท id และช่องว่างภายในอีก 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