📜 ⬆️ ⬇️

Updating the tree model in Qt

Good day to all! In this article I want to talk about the difficulties encountered when displaying and updating the tree structure using QTreeView and QAbstractItemModel. I will also offer a bicycle that I created to circumvent these difficulties.

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

In my project, I encountered another difficulty - the update from the outside. The fact is that QAbstractItemModel requires that before any actions with items you need to explicitly indicate which items are specifically deleted, added, moved. As I understand, it is assumed that the model will be edited only by means of View or through methods QAbstractItemModel. However, if I work with someone else’s model from a library that does not know how to “correctly” notify about their changes, or the model is intensively edited so that sending messages about its changes becomes expensive, then updating the model structure becomes more complicated.

To solve this update and simplify the creation of the QAbstractItemModel implementation. I decided to use the following approach: make a simple interface for querying the tree structure:
')
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 own QAbstractItemModel, in which the structure will be cached and lazily loaded as needed. And update the model to make a simple caching of the cached structure with the VirtualModelAdapter.

Thus, instead of the heap of beginInsertRows / endInsertRows and beginRemoveRows / endRemoveRows calls, you can enclose the model update in the beginUpdate () endUpdate () brackets and synchronize at the end of the update. Note that only the structure (not the data) is cached, and only that part of it that is opened by the user. No sooner said than done. For tree caching, I used the following structure:

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

And to update the structure of the model, I use a function that compares the list of elements and, if it does not match, inserts new ones and deletes 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<int>(nodes.size())) { bool finishing = srcCur >= static_cast<int>(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 the View calls to index (), parent (), etc. are broadcast to the cache, and data () returns an empty QVariant (). Upon completion of the update of the structure, you call endUpdate () and synchronization with all inserts and deletions occurs and the View is redrawn.

As an example, I made the following section structure:

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

Now, to display it, 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 it does not leave me feeling that the problem can be solved more easily. 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 welcome.

Source: https://habr.com/ru/post/250431/


All Articles