'simpletreemodel' based QAbstractItemModel: NOT editable but DYNAMIC
-
Hello,
I'm developing a simple application based on a classic model/view structure.
I've read all the documentation at https://doc.qt.io/qt-5/modelview.html and https://doc.qt.io/qt-5/model-view-programming.html and also based my work on the https://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html example, but I have still some questions/doubts about how to operate.My Model is basically 'TreeModel', heritating from QAbstractItemModel.
My goal is to display the progress of some tasks in two columns: an unique 'id' column and a 'progress' one. Tasks can be 'complex', i.e., can have children, so, as in the simpletreemodel, I have a *rootItem with a tree structure beneath it.
Data is coming from a JSON file that I parse recursively to create the tree structure.
My model is NOT editable, i.e. user can't modify data on the view, data is read-only, BUT is dynamic, meaning that new data arrive from time to time from an external server : it is needed to update the progress of already existing ID's (ongoing tasks), new ID's (new tasks), 'leafs' on the tree that become nodes (simple tasks that transform into 'complex' task: creation of new children on the go), IDs that disappear (finished tasks)...
The way I work so far is:
The first time I get data from server, I parse it and 'fill' the *rootItem/tree structure. It shows perfectly on my view, respecting the tree structure.
MY ISSUES comes when I have to update the data:
On the fast and ugly version, I create a new *newDataRoot with incoming data and I swap it with *rootItem:void TreeModel::updateProgress(QJsonObject newProgress) { ProgressTreeItem *newDataRoot = new ProgressTreeItem(newProgress); setupJSONModelData(newProgress, newDataRoot); // parse JSON file, fill *newDataRoot structure same way as done with *rootItem beginResetModel(); delete rootItem; rootItem = newDataRoot; newDataRoot = nullptr; endResetModel(); }
It 'works', meaning displayed data is updated, but each time the model is reset, the view's expanded items are collapsed. I resolved this issue with this https://stackoverflow.com/questions/3253301/howto-restore-qtreeview-last-expanded-state .
And it works. Fine. But I still feel this is kinda of 'cheating', or at least a 'dirty' solution.
So, my idea to update the data on *rootItem structure is to update each 'node'/'leaf' of the tree structure using their unique 'id' to identify them.
- Anytime new data arrives, I create/fill another temporary *tmpRootItem.
- I create a function to iterate over all the nodes on *tmpRootItem, and make a search by 'id' on original *rootItem.
If 'id' existed, update that id's progress (case a)
If 'id' didn't exist (new task), add it to rootItem (case b) - Do reverse search: iterate over original *rootItem and search each 'id' on *tmpRootItem
if 'id' exists, do nothing
if 'id' does not exist, it corresponds to a finished task: delete that node. (case c)
My questions:
As data is not editable by user, I assume that model's setData() function shouldn't be implemented. Is this correct??
As data is dynamic, I should call signaldataChanged()
at the end of my algorithm withtopLeft
my 'first' index andbottomRight
my 'last' index??
Or should it better to call, in each of the cases on the algorithm:
* (case a)dataChanged()
signal on indexes that have changed
* (case b)rowsInserted()
signal on indexes that have been added ( new rows on the model, as new tasks are created)
* (case c)rowsRemoved()
signal in case the task has finished and thus no longer needed its display.I know that this seems a very personal/particular case of use, but I really feel that it could help lots of people (including myself) to learn more on Models and how they work.
Thank you very much!
-
Hello,
I'm developing a simple application based on a classic model/view structure.
I've read all the documentation at https://doc.qt.io/qt-5/modelview.html and https://doc.qt.io/qt-5/model-view-programming.html and also based my work on the https://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html example, but I have still some questions/doubts about how to operate.My Model is basically 'TreeModel', heritating from QAbstractItemModel.
My goal is to display the progress of some tasks in two columns: an unique 'id' column and a 'progress' one. Tasks can be 'complex', i.e., can have children, so, as in the simpletreemodel, I have a *rootItem with a tree structure beneath it.
Data is coming from a JSON file that I parse recursively to create the tree structure.
My model is NOT editable, i.e. user can't modify data on the view, data is read-only, BUT is dynamic, meaning that new data arrive from time to time from an external server : it is needed to update the progress of already existing ID's (ongoing tasks), new ID's (new tasks), 'leafs' on the tree that become nodes (simple tasks that transform into 'complex' task: creation of new children on the go), IDs that disappear (finished tasks)...
The way I work so far is:
The first time I get data from server, I parse it and 'fill' the *rootItem/tree structure. It shows perfectly on my view, respecting the tree structure.
MY ISSUES comes when I have to update the data:
On the fast and ugly version, I create a new *newDataRoot with incoming data and I swap it with *rootItem:void TreeModel::updateProgress(QJsonObject newProgress) { ProgressTreeItem *newDataRoot = new ProgressTreeItem(newProgress); setupJSONModelData(newProgress, newDataRoot); // parse JSON file, fill *newDataRoot structure same way as done with *rootItem beginResetModel(); delete rootItem; rootItem = newDataRoot; newDataRoot = nullptr; endResetModel(); }
It 'works', meaning displayed data is updated, but each time the model is reset, the view's expanded items are collapsed. I resolved this issue with this https://stackoverflow.com/questions/3253301/howto-restore-qtreeview-last-expanded-state .
And it works. Fine. But I still feel this is kinda of 'cheating', or at least a 'dirty' solution.
So, my idea to update the data on *rootItem structure is to update each 'node'/'leaf' of the tree structure using their unique 'id' to identify them.
- Anytime new data arrives, I create/fill another temporary *tmpRootItem.
- I create a function to iterate over all the nodes on *tmpRootItem, and make a search by 'id' on original *rootItem.
If 'id' existed, update that id's progress (case a)
If 'id' didn't exist (new task), add it to rootItem (case b) - Do reverse search: iterate over original *rootItem and search each 'id' on *tmpRootItem
if 'id' exists, do nothing
if 'id' does not exist, it corresponds to a finished task: delete that node. (case c)
My questions:
As data is not editable by user, I assume that model's setData() function shouldn't be implemented. Is this correct??
As data is dynamic, I should call signaldataChanged()
at the end of my algorithm withtopLeft
my 'first' index andbottomRight
my 'last' index??
Or should it better to call, in each of the cases on the algorithm:
* (case a)dataChanged()
signal on indexes that have changed
* (case b)rowsInserted()
signal on indexes that have been added ( new rows on the model, as new tasks are created)
* (case c)rowsRemoved()
signal in case the task has finished and thus no longer needed its display.I know that this seems a very personal/particular case of use, but I really feel that it could help lots of people (including myself) to learn more on Models and how they work.
Thank you very much!
But I still feel this is kinda of 'cheating', or at least a 'dirty' solution.
it is
Anytime new data arrives, I create/fill another temporary *tmpRootItem.
No need to create a new root. just act on the one that already exists
As data is not editable by user, I assume that model's setData() function shouldn't be implemented. Is this correct??
correct
Or should it better to call, in each of the cases on the algorithm:
- (case a) dataChanged() signal on indexes that have changed
- (case b) rowsInserted() signal on indexes that have been added ( new rows on the model, as new tasks are created)
- (case c) rowsRemoved() signal in case the task has finished and thus no longer needed its display.
As a side note, indataChanged
,topLeft
andbottomRight
must have the same parent (i.e. you have to send a different signal for each affected branch-level of the treebut I really feel that it could help lots of people (including myself) to learn more on Models and how they work.
Best advice ever is to use the model test, it spots SOOOOO many mistakes you could make.
-
Hi there,
Thanks again for the advice on the Model Test : it was really helpful!So I finally got something working and I wanted to shared it, to help people, resolve some doubts and to find possible improving suggestions.
What I've got so far:
For the first data coming (a json file that I transform into aQJsonObject
, I create the model, I set it to the view, I iterate recursively over the file and I create/fill theTreeItem *rootItem
.
Each of the 'nodes' of the tree have an unique ID, that I use later to get the QModelIndex of internal data as proposed in https://wiki.qt.io/Technical_FAQ#How_can_a_QModelIndex_be_retrived_from_the_model_for_an_internal_data_item.3F / Option "Use unique identifiers**"Then, anytime new data is coming, I iterate recursively on the new file/QJsonObject and do:
a) Search each 'id' on*rootItem
(I have a recursive function to look for an ID in any tree node, including its children).- if 'id' exists, update its data. QUESTIONS BELOW on
match()
andcreateIndex
- if not, create the TreeItem structure of this node, including the possible children, get what node in
*rootItem
structure should be the parent of the new node and add/append it to that node.
My Code:
void TreeModel::updateModelData(const QJsonObject &p_object, TreeItem *p_TreeItem) { /* * Iterate over all p_object tasks: * if their ID existed, update item and view * if not, create TreeItem, add it to rootItem structure and update view */ if (!p_object.isEmpty()) { QJsonArray children = p_object["children"].toArray(); if (!children.isEmpty()) { for (auto child : qAsConst(children)) { TreeItem *progItem = p_TreeItem->searchId(child["id"].toString()); if (progItem == nullptr) { // 'ID' NOT FOUND : ADD IT TO p_TreeItem TreeItem *parentItem = p_TreeItem->searchId(p_object["id"].toString()); // build TreeItem from child QJsonObject , INCLUDING CHILDREN and children of children and... // parentProgItem will be the parent of all the structure. It could be rootItem TreeItem *newItem = new TreeItem(child.toObject(), parentProgItem); setupJSONModelData(child.toObject(), newItem); // QUESTION/DOUBT 0 // As a new node is added to the data structure, createIndex should be called for these new nodes. Is this correct?? QModelIndex parentIndex; if (parentItem == rootItem) { // If parent item is rootItem, then create the index // The null/empty QModelndex represents the root of a tree model parentIndex = QModelIndex(); } else { parentIndex = createIndex(parentItem->row(), 0, parentItem); } int first = parentProgItem->childCount(); // QUESTION/DOUBT 1: // As the following works, I guess that it's correct, but in the documentation... // ...I find it not clear that when adding a row, if that 'node' has children then ... // ... those children's rows are automatically added. Lost quite time trying to call... // begin/endInsertRows for EACH child-node of the new node... beginInsertRows(parentIndex, first, first); parentProgItem->appendChild(newItem); endInsertRows(); } else { // udpate values of the existing nodes. progItem->updateItem(child.toObject()); // look for THE index that match the ID to call dataChanged() // QUESTION 2 : QModelIndexList matchesOnID = match(index(0, 1), Qt::UserRole, progItem->id(), 1, Qt::MatchRecursive); for (QModelIndex match : qAsConst(matchesOnID)) { // QUESTION 3 : QModelIndex nextColIndex = createIndex(match.row(), match.column() + 1, match.internalPointer()); emit dataChanged(match, nextColIndex); } // recursive call if (child["type"].toString() == "Node") { updateModelData(child.toObject(), p_progressTreeItem); } } } } } }
QUESTION 2:
TreeModel::data() returns the ID forrole == Qt::UserRole
.
That ID value is displayed in the second column of the view.
According to Qt docQAIM::match()
"returns a list of indexes for the items in the column of the start index where data stored under the given role matches the specified value"
So, if I look for matches of my ID, what should it be the start QModelIndex ??
I've tried bothmatch(index(0, 1), Qt::UserRole, progItem->id(), 1, Qt::MatchRecursive);
andmatch(index(0, 0), Qt::UserRole,...)
and they 'worked' both, but I don't if it's because the Recursive nature of the search or another reason. Should the start index correspond to the colum 1 (the column where ID is displayed) or the view and the model are not linked for this matter??QUESTION 3:
I create an indexnextColIndex
based on the previousmatch
to try to apply thedataChanged
signal to the whole row : the third column of the view is the one that changes really(display needed to be updated), it shows some data associated to the item.To end it up: how to handle the items that 'dissapear', finished tasks:
So, related to "No need to create a new root. just act on the one that already exists", I didn't manage to act directly on the original root (lack of programming competences, I guess... didn't figure it out how to do it): I need to create a temporary root tree with new incoming data to compare it to the previously existing one. If someone has pseudocode on how to implement this, I'd be thankfull to hear about it!
My 'algorithm':
Iterate overrootItem
. Search each 'id' onnewRootItem
- If 'id' does not exist (the task with that 'id' is finished), delete that 'id' (and possible children) from
rootItem
structure.
As deleting items (in the underlying TreeItem
QVector<TreeItem*> m_childItems;
structure) caused memory issues, I added aQMutableVectorIterator<TreeItem *> *m_childIterator;
member on ItemTree class and expose itsnext()
,hasNext()
,toFront()
andremove()
functions.
The code:void TreeModel::recursiveCleanProgress(TreeItem *p_oldProgressParent, TreeItem *p_newProgressParent) { p_oldProgressParent->iteratorToFront(); while (p_oldProgressParent->iteratorHasNext()) { CProgressTreeItem *nextChild = p_oldProgressParent->iteratorNext(); CProgressTreeItem *progressItem = p_newProgressParent->searchId(nextChild->id()); if (progressItem == nullptr) { int childRow = nextChild->row(); // Look for PARENT QModelIndex : needed to pass to beginRemoveRows QModelIndexList matches; if (p_oldProgressParent->id() == "") matches.push_back(QModelIndex()); else matches = match(index(0, 1, QModelIndex()), Qt::UserRole, p_oldProgressParent->id(), 1, Qt::MatchRecursive); for (QModelIndex match : qAsConst(matches)) { beginRemoveRows(match, childRow, childRow); // remove() does not 'destroy' the item from the QVector: delete it later p_oldProgressParent->iteratorRemove(); delete nextChild; endRemoveRows(); } } else { recursiveCleanProgress(nextChild, p_newProgressParent); } } }
ps : I'm reopening this thread because I feel it can be a useful complement to the previous questions. If another thread should be open, no problem with that.
- if 'id' exists, update its data. QUESTIONS BELOW on