Hierarchical models in Qt
- Tutorial
I continue the topic of creating models using Qt MV. The last time was a critical article about how to do. We pass to the positive part.
To create flat models for lists and tables, you can use the QAbstractListModel and QAbstractTableModel presets . Bringing them to readiness is not difficult, so there is no need to consider them in detail.
Creating hierarchical models is a more difficult task. It will be discussed in this article.
In general, Qt allows you to create not only tree-like models, but also models of more complex configurations and dimensions. For example, a table in which each element is grouping for a subtable. Despite this, in most cases when they talk about hierarchical models, they mean trees. It is the process of creating trees that I want to highlight.
Another introductory remark concerns the essence of the model, which I will create as an example. I wanted to choose something specific and universal, so I decided to create an example model that displays the file system. It does not suffer from completeness and completeness, so it is unlikely that someone will want to use it seriously (especially since there is already a QFileSystemModel ), but for an example it is quite suitable. In the next article, if there is one, I’m going to build several proxies around this model.
Additional information about data organization can be used in the design process, so creating models of the first type usually does not cause problems. The article will focus on models of the second type.
All types of data that you have to deal with when developing tree models are actually similar. Therefore, it will not be difficult to highlight some general recommendations regarding the internal organization of data:
In the case of a file system model, I will use QFileInfo to store information about each node. In addition, you will need to store information about the child nodes and the parent node. Additionally, you will need information about whether the search for child nodes was performed or not.
Business data (
We get the following internal data structure:
When creating a tree, I will construct a list of nodes corresponding to the root objects of file systems, and I will load their children as necessary:
There will be several columns in the tree:
If you need to implement a hierarchical model, then there is nothing left but to inherit from QAbstractItemModel . In order to implement the simplest model, you need to write an implementation of only five functions:
However, the implementation of the first two usually accounts for 80% of the problems associated with the creation of hierarchical models. The thing is that they are called very often, so the use of algorithms in them is more complicated than O (1), generally speaking, it is not desirable.
I suggest storing a pointer to
With
There are two options - either to store information about its position with each node, or to determine this position each time again. The trouble with the first is that when you add and remove nodes, all the underlying nodes will have to be updated. What I really did not want. Therefore, I chose the second option, despite its computational complexity. In a real model, I would stick to that choice until I could prove that this is a bottleneck.
Implementation
Implementation
Now you can display the model using
However, what is it! Root elements cannot be expanded.
Indeed, because the data for them has not yet been downloaded.
The first function should return
Useful here
For loading we will use the functions provided
We do the loading, run the program, but there is no result. The root nodes are also impossible to deploy. The thing is that it
In this case, this behavior is not suitable. We redefine the function
You can determine for sure whether the directory is empty, but it’s a rather expensive operation, whether you use it is up to you.
Now the model is working, you can view the folders.
I think we can end here, for the sake of completeness, we could add the function of renaming files and folders, creating directories and tracking changes to the file system. But this clearly goes beyond the permitted article on Habr. I posted a slightly more advanced example on GitHub .
To create flat models for lists and tables, you can use the QAbstractListModel and QAbstractTableModel presets . Bringing them to readiness is not difficult, so there is no need to consider them in detail.
Creating hierarchical models is a more difficult task. It will be discussed in this article.
In general, Qt allows you to create not only tree-like models, but also models of more complex configurations and dimensions. For example, a table in which each element is grouping for a subtable. Despite this, in most cases when they talk about hierarchical models, they mean trees. It is the process of creating trees that I want to highlight.
Figure: Table in table
Another introductory remark concerns the essence of the model, which I will create as an example. I wanted to choose something specific and universal, so I decided to create an example model that displays the file system. It does not suffer from completeness and completeness, so it is unlikely that someone will want to use it seriously (especially since there is already a QFileSystemModel ), but for an example it is quite suitable. In the next article, if there is one, I’m going to build several proxies around this model.
Design
First of all, you need to decide on the internal data structure . Here I would highlight 2 main areas:- Models with a well-defined structure or small nesting.
An example would be the property editor in Qt Designer. In this case, all the information about the position of a particular model element can be stored in the index itself ( QModelIndex ) usinginternalId
. In the case of the property editor,internalId
you can save the property group identifier in.
Another example is a notebook model. Obviously, a "record Lol4t0 » included in subgroup Lo , which in turn is included in group L . And vice versa - the Lo group includes those and only those records that begin with the prefix “ Lo ”. This is what I call a well-defined structure. - Models without a well-defined structure. The file system model refers to these. Knowing only the name of the Documents folder , generally speaking, it is impossible to determine which folder it is in and which folders it contains.
Additional information about data organization can be used in the design process, so creating models of the first type usually does not cause problems. The article will focus on models of the second type.
All types of data that you have to deal with when developing tree models are actually similar. Therefore, it will not be difficult to highlight some general recommendations regarding the internal organization of data:
- Need to store node data
- There must be a one-to-one relationship between
QModelIndex
and an element of the internal data structure - Each node must contain links to the parent node and child nodes
In the case of a file system model, I will use QFileInfo to store information about each node. In addition, you will need to store information about the child nodes and the parent node. Additionally, you will need information about whether the search for child nodes was performed or not.
Business data ( QFileInfo
) had to be wrapped with service information. In most cases, it is impossible to do without it. If the data in the subject area already supports the hierarchy, you can use it, but I have never encountered the case when the initial data would contain all the necessary information.
We get the following internal data structure:
struct FilesystemModel::NodeInfo
{
QFileInfo fileInfo; // информация об узле
QVector children; // список дочерних узлов
NodeInfo* parent; // ссылка на родительский узел
bool mapped; // производился ли поиск дочерних узлов.
};
When creating a tree, I will construct a list of nodes corresponding to the root objects of file systems, and I will load their children as necessary:
typedef QVector NodeInfoList;
NodeInfoList _nodes; // список корневых узлов файловых систем
There will be several columns in the tree:
enum Columns
{
RamificationColumn, // столбец, по которому производится ветвление, всегда первый.
// Другого варианта не поддерживает QTreeView
NameColumn = RamificationColumn, // столбец с именем узла
ModificationDateColumn, // столбец с датой изменения узла
SizeColumn, // столбец с размером файла
ColumnCount // число столбцов
};
Minimal implementation
Once you have decided on the data storage structure, you can begin to implement the model.If you need to implement a hierarchical model, then there is nothing left but to inherit from QAbstractItemModel . In order to implement the simplest model, you need to write an implementation of only five functions:
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const;
virtual QModelIndex parent(const QModelIndex &child) const;
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
However, the implementation of the first two usually accounts for 80% of the problems associated with the creation of hierarchical models. The thing is that they are called very often, so the use of algorithms in them is more complicated than O (1), generally speaking, it is not desirable.
I suggest storing a pointer to
NodeInfo
in an internalPointer
index. In most cases, they do just that. When implementing, index
in no case should non-existent indexes be returned. At the same time, one does not need to expect that no one will request such an index. There is a very convenient function for checking the existence of an index hasIndex
.QModelIndex FilesystemModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
if (!parent.isValid()) { // запрашивают индексы корневых узлов
return createIndex(row, column, const_cast(&_nodes[row]));
}
NodeInfo* parentInfo = static_cast(parent.internalPointer());
return createIndex(row, column, &parentInfo->children[row]);
}
With
parent
everything a little more complicated. Despite the fact that you can always find the NodeInfo
parent element at a given index , to create the index of the parent element, you must also know its position among the "brothers". There are two options - either to store information about its position with each node, or to determine this position each time again. The trouble with the first is that when you add and remove nodes, all the underlying nodes will have to be updated. What I really did not want. Therefore, I chose the second option, despite its computational complexity. In a real model, I would stick to that choice until I could prove that this is a bottleneck.
QModelIndex FilesystemModel::parent(const QModelIndex &child) const
{
if (!child.isValid()) {
return QModelIndex();
}
NodeInfo* childInfo = static_cast(child.internalPointer());
NodeInfo* parentInfo = childInfo->parent;
if (parentInfo != 0) { // parent запрашивается не у корневого элемента
return createIndex(findRow(parentInfo), RamificationColumn, parentInfo);
}
else {
return QModelIndex();
}
}
int FilesystemModel::findRow(const NodeInfo *nodeInfo) const
{
const NodeInfoList& parentInfoChildren = nodeInfo->parent != 0 ? nodeInfo->parent->children: _nodes;
NodeInfoList::const_iterator position = qFind(parentInfoChildren, *nodeInfo);
return std::distance(parentInfoChildren.begin(), position);
}
Implementation
rowCount
and columnCount
trivial: in the first case we can always determine the number of child nodes NodeInfo::children::size
, and the number of columns is fixed.int FilesystemModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return _nodes.size();
}
const NodeInfo* parentInfo = static_cast(parent.internalPointer());
return parentInfo->children.size();
}
int FilesystemModel::columnCount(const QModelIndex &) const
{
return ColumnCount;
}
Implementation
data
also does not represent anything complicated, we get all the necessary information from QFileInfo
. At a minimum, you must implement role support Qt::DisplayRole
to display text in view
and Qt::EditRole
, if editing is provided. Data received from the role model Qt::EditRole
will be loaded into the editor. Moreover, the data that the model returns when requested with Qt::DisplayRole
and Qt::EditRole
may vary. For example, we will display files without extensions, and edit with the extension.Function code data
In order for the model to come to life, it remains to fill in the root nodes:
QVariant FilesystemModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
const NodeInfo* nodeInfo = static_cast(index.internalPointer());
const QFileInfo& fileInfo = nodeInfo->fileInfo;
switch (index.column()) {
case NameColumn:
return nameData(fileInfo, role);
case ModificationDateColumn:
if (role == Qt::DisplayRole) {
return fileInfo.lastModified();
}
break;
case SizeColumn:
if (role == Qt::DisplayRole) {
return fileInfo.isDir()? QVariant(): fileInfo.size();
}
break;
default:
break;
}
return QVariant();
}
QVariant FilesystemModel::nameData(const QFileInfo &fileInfo, int role) const
{
switch (role) {
case Qt::EditRole:
return fileInfo.fileName();
case Qt::DisplayRole:
if (fileInfo.isRoot()) {
return fileInfo.absoluteFilePath();
}
else if (fileInfo.isDir()){
return fileInfo.fileName();
}
else {
return fileInfo.completeBaseName();
}
default:
return QVariant();
}
Q_UNREACHABLE();
}
In order for the model to come to life, it remains to fill in the root nodes:
void FilesystemModel::fetchRootDirectory()
{
const QFileInfoList drives = QDir::drives();
qCopy(drives.begin(), drives.end(), std::back_inserter(_nodes));
}
FilesystemModel::FilesystemModel(QObject *parent) :
QAbstractItemModel(parent)
{
fetchRootDirectory();
}
Now you can display the model using
QTreeView
and see the result. However, what is it! Root elements cannot be expanded.
Indeed, because the data for them has not yet been downloaded.
Dynamic data loading
To implement automatic data loading as needed, the following API is implemented in Qt:bool canFetchMore(const QModelIndex &parent) const;
void fetchMore(const QModelIndex &parent);
The first function should return
true
when the data for the given parent can be loaded, and the second should load the data itself. Useful here
NodeInfo::mapped
. Data can be loaded when mapped == false
.bool FilesystemModel::canFetchMore(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return false;
}
const NodeInfo* parentInfo = static_cast(parent.internalPointer());
return !parentInfo->mapped;
}
For loading we will use the functions provided
QDir
. At the same time, do not forget to use beginInsertRows
it endInsertRows
when changing the number of lines. Unfortunately, it QTreeView
performs loading only when trying to expand a node, and does not try to load new data when scrolling through the list. Therefore, there is nothing left but to download the entire list of child nodes as a whole. You can fix this behavior, except by creating your own display component.void FilesystemModel::fetchMore(const QModelIndex &parent)
{
NodeInfo* parentInfo = static_cast(parent.internalPointer());
const QFileInfo& fileInfo = parentInfo->fileInfo;
QDir dir = QDir(fileInfo.absoluteFilePath());
QFileInfoList children = dir.entryInfoList(QStringList(), QDir::AllEntries | QDir::NoDotAndDotDot, QDir::Name);
beginInsertRows(parent, 0, children.size() - 1);
parentInfo->children.reserve(children.size());
for (const QFileInfo& entry: children) {
NodeInfo nodeInfo(entry, parentInfo);
nodeInfo.mapped = !entry.isDir();
parentInfo->children.push_back(std::move(nodeInfo));
}
parentInfo->mapped = true;
endInsertRows();
}
We do the loading, run the program, but there is no result. The root nodes are also impossible to deploy. The thing is that it
QTreeView
uses a function hasChildren
to check whether a node has children, and believes that only nodes that have children can be expanded. hasChildren
it returns by default true
only when the number of rows and the number of columns for the parent node is greater than 0. In this case, this behavior is not suitable. We redefine the function
hasChildren
so that it returns true
for the given node when it definitely has or can (when mapped ==false
) have child nodes. You can determine for sure whether the directory is empty, but it’s a rather expensive operation, whether you use it is up to you.
bool FilesystemModel::hasChildren(const QModelIndex &parent) const
{
if (parent.isValid()) {
const NodeInfo* parentInfo = static_cast(parent.internalPointer());
Q_ASSERT(parentInfo != 0);
if (!parentInfo->mapped) {
return true;//QDir(parentInfo->fileInfo.absoluteFilePath()).count() > 0; -- точное определение того, что директория не пуста
}
}
return QAbstractItemModel::hasChildren(parent);
}
Now the model is working, you can view the folders.
I think we can end here, for the sake of completeness, we could add the function of renaming files and folders, creating directories and tracking changes to the file system. But this clearly goes beyond the permitted article on Habr. I posted a slightly more advanced example on GitHub .