GitRaven: How to use QTreeView with a custom model class

Hi,
This blog post will cover how to create a custom model class based on QAbstractItemModel to render custom data with QTreeView
in C++.
Theory
General idea:
Create a new class
RavenTreeModel
based onQAbstractTreeModel
.We need to override few methods from parent class to succeed -
index
,parent
,rowCount
,columnCount
anddata
.
index
- It is used to return a model index for a given row, column and an optional parent model index.data
- It is used to return the data we would like to show for the given row and column. This could be a tree item’s text, icon, tooltip text when hovered, etc.parent
- This function returns the parent for a given model.
So let's say you have:A -> B -> C
Here,A
is the parent ofB
andB
is the parent of C. So when we traverse the custom model while rendering, we determine and return the parent of each node or we return an invalid index (which Qt accepts as a valid model index).rowCount
- We compute and return the number of children for a given model index. If the given index is invalid, we return child count of the root node.
“Root Node” and QTreeView
:
Consider the following tree structure which we need to represent:
A -> B -> C
// A is the root node in this scenario where B and C are child nodes of A
// B is the child node of A but is also a parent of C
// C is the child node of A and is also the leaf node in this tree.
In order to render this structure in QTreeView
, we need to introduce a “container” node to host all the “root” nodes from the data description above. We will track this root node in code with a pointer inside our model as a member variable.
Why?
The
QTreeView
expects a single root item for the model, from which all other tree items are derived. This root item acts as the starting point of the entire tree structure.The
QAbstractItemModel
needs a root node to represent the topmost parent of your data. Without it, there wouldn't be a clear point from which the model’s hierarchy starts, which can cause issues in handling the tree’s structure and rendering.
—
ChatGPT
Here’s the new flow:
Root -> A -> B -> C
Root -> D -> E -> F
Here,A
andD
are the actual user-visible "root" nodes of the tree however both these nodes are child nodes ofRoot
.
AI Usage
As explained in my previous post, this section will explain the AI usage for this part of the project.
I struggled to get the TreeView to work. It took me almost 8 hours of time at night to come up with the code shown below.
Finally, after admitting defeat, I resorted to ChatGPT and it helped me fix the issues. The bugs were in both index
and parent
functions.
Here’s my early attempt based on Star Delegate example code and some custom changes.
// NOT WORKING!!!
QModelIndex RavenTreeModel::index(int row, int column, const QModelIndex &parent) const
{
qDebug() << "RavenTreeModel::index called";
if (!hasIndex(row, column, parent))
return {};
RavenTreeItem *parentItem = parent.isValid()
? static_cast<RavenTreeItem*>(parent.internalPointer())
: rootItem.get();
if (auto *childItem = parentItem->child(row))
return createIndex(row, column, childItem);
return {};
return QModelIndex();
}
QModelIndex RavenTreeModel::parent(const QModelIndex &child) const
{
qDebug() << "RavenTreeModel::parent called";
// auto parent = child.data(12);
const QModelIndex *val = nullptr;
for (auto item: m_items)
{
if (&item.name() == child.internalPointer())
{
qDebug() << item. << child;
val = &child;
}
}
return *val;
}
Implementation
Here's the model's header file - I use a custom data struct RavenTreeItem
to hold the data of each node. QModelIndex
is the class that tracks model index in the tree.
RavenTreeModel.h
#ifndef RAVENTREEMODEL_H
#define RAVENTREEMODEL_H
#include <QAbstractItemModel>
#include <QObject>
class RavenTreeModel: public QAbstractItemModel
{
Q_OBJECT
public:
struct RavenTreeItem {
QString name;
QString fullPath;
QString absolutePath;
bool binary;
QList<RavenTreeItem*> children;
int *flag;
};
explicit RavenTreeModel(QObject *parent = nullptr);
~RavenTreeModel() override;
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
// Helper functions
RavenTreeItem* getRootNode();
static RavenTreeItem* createNode(const QString &name, const QString &fullPath, const QString &absPath, int flag, bool binary);
private:
RavenTreeItem *rootNode; // <-- "Root" node that acts like container of all the tree nodes.
};
#endif // RAVENTREEMODEL_H
I have replaced the data type for
flag
property in my code for keeping things simple for this example. Hence, this property, in it's current state, might not make sense.
Let's see the model class' actual implementation. RavenTreeModel.cpp
#include "raventreemodel.h"
#include <QIcon>
RavenTreeModel::RavenTreeModel(QObject *parent)
: QAbstractItemModel(parent),
rootNode(createNode("Root", "", "", 0, false))
{}
RavenTreeModel::~RavenTreeModel() {
qDeleteAll(rootNode->children);
delete rootNode;
};
QModelIndex RavenTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) return QModelIndex();
RavenTreeItem *parentNode = parent.isValid() ? static_cast<RavenTreeItem*>(parent.internalPointer()) : rootNode;
RavenTreeItem *childNode = parentNode->children.value(row);
// Ensure that the node is valid before creating the index
if (!childNode) QModelIndex();
return createIndex(row, column, childNode);
}
QModelIndex RavenTreeModel::parent(const QModelIndex &child) const
{
if (!child.isValid()) return QModelIndex();
RavenTreeItem *childNode = static_cast<RavenTreeItem*>(child.internalPointer());
RavenTreeItem *parentNode = nullptr;
// If it's rootNode, return invalid QModelIndex
if (childNode == rootNode) return QModelIndex();
// Find the parent node from `rootNode` children
for (RavenTreeItem* node : static_cast<const QList<RavenTreeItem*>>(rootNode->children))
{
if (node->children.contains(childNode)) {
parentNode = node;
break;
}
}
// Handle invalid case
if (!parentNode) return QModelIndex();
// Found parentNode, compute row and return expected output
int row = parentNode->children.indexOf(childNode);
return createIndex(row, 0, parentNode);
}
int RavenTreeModel::rowCount(const QModelIndex &parent) const
{
// Find row for given QModelIndex (if valid) or fallback to `rootNode`
auto node = parent.isValid() ? static_cast<RavenTreeItem*>(parent.internalPointer()) : rootNode;
return node->children.count();
}
int RavenTreeModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
QVariant RavenTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return QVariant();
RavenTreeItem *node = static_cast<RavenTreeItem*>(index.internalPointer());
if (role == Qt::DisplayRole)
{
return node->name;
}
if (role == Qt::DecorationRole)
{
auto iconName = node->children.size() > 0 ? "folder-symbolic" : "filename-title-amarok";
auto iconProvider = QIcon::fromTheme(iconName);
return iconProvider;
}
if (role == Qt::ToolTipRole)
{
return node->fullPath;
}
return QVariant();
}
RavenTreeModel::RavenTreeItem *RavenTreeModel::getRootNode()
{
return rootNode;
}
RavenTreeModel::RavenTreeItem *RavenTreeModel::createNode(const QString &name, const QString &fullPath, const QString &absPath, int flag, bool binary)
{
RavenTreeItem *node = new RavenTreeItem;
node->name = name;
node->fullPath = fullPath;
node->absolutePath = absPath;
node->flag = &flag;
node->binary = binary;
node->children.clear(); // Ensure the children list is initialized
return node;
}
Let's go over each functions:
index():
QModelIndex RavenTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) return QModelIndex();
RavenTreeItem *parentNode = parent.isValid() ? static_cast<RavenTreeItem*>(parent.internalPointer()) : rootNode;
RavenTreeItem *childNode = parentNode->children.value(row);
// Ensure that the node is valid before creating the index
if (!childNode) QModelIndex();
return createIndex(row, column, childNode);
}
In this code, I check whether we have a valid model index for the given row and column. If I do not, we create an invalid model index of our own.
Next, we grab the data struct
RavenTreeItem
from the parent model index.We have to do this check in order to deal with “parent-less nodes” like the
A
node in my example above. It doesn't have a parent node (in our representation) howeverQTreeView
expects a single root node at the top-most level.Next, we compute the child node from the struct and return a model index for this newly created item for a given row and column.
Lastly, we use
createIndex
function to create a model index for the givenchildNode
struct at the given row and column and return it.
Next, let's see the parent()
function implementation:
parent():
QModelIndex RavenTreeModel::parent(const QModelIndex &child) const
{
if (!child.isValid()) return QModelIndex();
RavenTreeItem *childNode = static_cast<RavenTreeItem*>(child.internalPointer());
RavenTreeItem *parentNode = nullptr;
// If it's rootNode, return invalid QModelIndex
if (childNode == rootNode) return QModelIndex();
// Find the parent node from `rootNode` children
for (RavenTreeItem* node : static_cast<const QList<RavenTreeItem*>>(rootNode->children))
{
if (node->children.contains(childNode)) {
parentNode = node;
break;
}
}
// Handle invalid case
if (!parentNode) return QModelIndex();
// Found parentNode, compute row and return expected output
int row = parentNode->children.indexOf(childNode);
return createIndex(row, 0, parentNode);
}
We check if the model index is valid and if not, we return an invalid model index of our own (same as above).
We grab the custom data struct of the given model index and cast it to our custom struct type (same as above).
Now comes the interesting part, if the child node we just grabbed from model index is the same as our "Root" node (container node) we return an invalid model index. This is expected because
rootNode
is the top-most node with no parent node.Next, we iterate over all the child nodes of
rootNode
to locate the parent node for the given model index (viachildNode
) and return new model index based on this information.If the node couldn’t be located, we return an invalid model index.
rowCount() & columnCount():
int RavenTreeModel::rowCount(const QModelIndex &parent) const
{
// Find row for given QModelIndex (if valid) or fallback to `rootNode`
auto node = parent.isValid() ? static_cast<RavenTreeItem*>(parent.internalPointer()) : rootNode;
return node->children.count();
}
int RavenTreeModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
- In the case of row count, we return the number of child nodes for a given valid model index. If the given model index is invalid, we return
rootNode
's child count as fallback.
I am assuming this means we have just started rendering the tree and we are currently at the top-most tree node. It might also be an invalid state but I am not sure how that can happen.
If you have more details, I am very interested in learning more. Please comment below or tag me on social media with the details. :)
- For
columnCount
, I return1
because my UI doesn't require any more columns.
Note: If you change the column count, my code won’t work for your use case. You might need to make additional tweaks. See Star Delegate example implementation for more details.
data():
QVariant RavenTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return QVariant();
RavenTreeItem *node = static_cast<RavenTreeItem*>(index.internalPointer());
if (role == Qt::DisplayRole)
{
return node->name;
}
if (role == Qt::DecorationRole)
{
auto iconName = node->children.size() > 0 ? "folder-symbolic" : "filename-title-amarok";
auto iconProvider = QIcon::fromTheme(iconName);
return iconProvider;
}
if (role == Qt::ToolTipRole)
{
return node->fullPath;
}
// Default value??
return QVariant();
}
We check if the model index is valid or return an invalid index of our own.
Grab the data from model index and cast it to our custom data struct.
Next, we need to know what kind of data is being rendered at the moment. These are described by
roles
where each role has it's own part to play. You can read more about Roles in Qt here.I’m interested in only 3 roles:
-DisplayRole
for displaying text on each tree node
-DecorationRole
for showing a folder or file icon on each tree node
-ToolTipRole
for showing tooltip text when user hovers on a tree node
createNode():
Lastly, here's createNode
function which generates new custom nodes inside my model.
RavenTreeModel::RavenTreeItem *RavenTreeModel::createNode(const QString &name, const QString &fullPath, const QString &absPath, int flag, bool binary)
{
RavenTreeItem *node = new RavenTreeItem;
node->name = name;
node->fullPath = fullPath;
node->absolutePath = absPath;
node->flag = &flag;
node->binary = binary;
node->children.clear(); // Ensure the children list is initialized
return node;
}
Results
Here’s a screenshot of the final result. I realize this isn’t much in terms of looks but it serves as a very good starting point for building something complex.
You will see how I customized this UI in future posts. :)
Conclusion
I hope you learned something new in this post. This is my first C++ project so there may be better way(s) to handle certain scenarios.
Please leave a comment below on your thoughts. You can also @
me on Mastodon here.
This project isn’t currently hosted anywhere yet. The code shown here is a few weeks old as of writing however it is good enough to be a starting point.
I don’t even use Git to manage the code. I have a bunch of _final_final
files to sort. I can see the irony here but I have yet to decide on project’s license. More details in my previous post.
Bye for now :)
Subscribe to my newsletter
Read articles from Surya Teja Karra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by