Updating the tree model in Qt

Good day to all! In this article I want to talk about the difficulties encountered in displaying and updating the tree structure using QTreeView and QAbstractItemModel. I will also offer the bike that I created to get around these difficulties.

To display data, Qt uses the ModelView paradigm in which the model must be implemented by the QAbstractItemModel descendants. This class is made conveniently, but the hierarchy support, as it seemed to me, is sewn somewhere on the side and is not very convenient. Building the right tree model, as developers admit in the documentation, is not an easy task, and even ModelTest , designed to help debug it, does not always help identify errors in the model.

In my project, I faced one more difficulty - updating from the outside. The fact is that QAbstractItemModel requires that before any actions with elements it is required to explicitly indicate which elements are specifically deleted, added, moved. As I understand it, it is assumed that the model will be edited only through View-s or through the QAbstractItemModel methods. However, if I work with someone else’s model from the library who doesn’t know how to “correctly” inform about my changes, or if the model is being intensively edited so that sending messages about its changes becomes unprofitable, then updating the structure of the model becomes complicated.

To solve the problem of such an update and simplify the creation of the QAbstractItemModel implementation. I decided to use the following approach: make a simple interface for querying the structure of the tree:

class VirtualModelAdapter {
public:
  // запрос структуры
  virtual int getItemsCount(void *parent) = 0;
  virtual void * getItem(void *parent, int index) = 0;
  virtual QVariant data(void *item, int role) = 0;
  // процедуры обновления
  void beginUpdate();
  void endUpdate();
}

and implement your QAbstractItemModel, in which the structure will be cached and lazily loaded as necessary. And make the model update a simple sihanizatsii cached structure with VirtualModelAdapter.

Thus, instead of a heap of calls to beginInsertRows / endInsertRows and beginRemoveRows / endRemoveRows, you can enclose the model update in the brackets beginUpdate () endUpdate () and synchronize when the update is complete. At the same time, note that only the structure (not data) is cached and only that part of it that is revealed by the user. No sooner said than done. To cache the tree, I used the following structure:

class InternalNode {
  InternalNode *parent;
  void *item;
  size_t parentIndex;  
  std::vector> children;  
}

And to update the structure of the model, I use a function that compares the list of elements and, if there is a mismatch, inserts new ones and removes unnecessary elements:

void VirtualTreeModel::syncNodeList(InternalNode &node, void *parent)
{
  InternalChildren &nodes = node.children;
  int srcStart = 0;
  int srcCur = srcStart;
  int destStart = 0;
  auto index = getIndex(node);
  while (srcCur <= static_cast(nodes.size()))
  {
    bool finishing = srcCur >= static_cast(nodes.size());
    int destCur = 0;
    InternalNode *curNode = nullptr;
    if (!finishing) {
      curNode = nodes[srcCur].get();
      destCur = m_adapter->indexOf(parent, curNode->item, destStart);
    }
    if (destCur >= 0)
    {
      // remove skipped source nodes
      if (srcCur > srcStart)
      {
        beginRemoveRows(index, srcStart, srcCur-1);
        node.eraseChildren(nodes.begin() + srcStart, nodes.begin() + srcCur);
        if (!finishing)
          srcCur = srcStart;
        endRemoveRows();
      }
      srcStart = srcCur + 1;
      if (finishing)
        destCur = m_adapter->getItemsCount(parent);
      // insert skipped new nodes
      if (destCur > destStart)
      {
        int insertCount = destCur - destStart;
        beginInsertRows(index, srcCur, srcCur + insertCount - 1);
        for (int i = 0, cur = srcCur; i < insertCount; i++, cur++)
        {
          void *obj = m_adapter->getItem(parent, destStart + i);
          auto newNode = new InternalNode(&node, obj, cur);
          nodes.emplace(nodes.begin() + cur, newNode);
        }
        node.insertedChildren(srcCur + insertCount);
        endInsertRows();
        srcCur += insertCount;
        destStart += insertCount;
      }
      destStart = destCur + 1;
      if (curNode && curNode->isInitialized(m_adapter))
      {
        syncNodeList(*curNode, curNode->item);
        srcStart = srcCur + 1;
      }
    }
    srcCur++;
  }
  node.childInitialized = true;
}

In essence, the following system is obtained: when the data structure begins to change after calling BeginUpdate (), all calls to View are addressed to index (), parent (), etc. are translated to the cache, and data () returns an empty QVariant (). Upon completion of the structure update, you call endUpdate () and synchronization occurs with all insertions and deletions and the View is redrawn.

As an example, I made the following section structure:

class Part {
  Part *parent;
  QString name;
  std::vector> subParts;
}

Now for its display it is enough for me to implement the following class:

сlass VirtualPartAdapter : public VirtualModelAdapter {
  int getItemsCount(void *parent) override;
  void * getItem(void *parent, int index) override;
  QVariant data(void *item, int role) override;
  void * getItemParent(void *item) override;
  Part *getValue(void * data);
};


And for any changes from the outside, we use the following approach:

  m_adapter->beginUpdate();
  Part* cur = currentPart();
  auto g1 = cur->add("NewType");
  g1->add("my class");
  g1->add("my struct");
  m_adapter->endUpdate();

As an even simpler alternative, you can call QueuedUpdate () before changing the data and then the structure will be updated automatically after processing the signal sent via Qt :: QueuedConnection:

  m_adapter-> QueuedUpdate();
  Part* cur = currentPart();
  auto g1 = cur->add("NewType");
  g1->add("my class");
  g1->add("my struct");


Conclusion


My experience with C ++ and Qt is not great and I have a feeling that the problem can be solved easier. In any case, I hope this method will be useful to someone. The full text and example can be found on github .

Comments and criticism are strongly appreciated.

Also popular now: