Exemple de modèle d'arbre modifiable
Cet exemple montre comment mettre en œuvre un modèle d'arbre simple basé sur les éléments, qui peut être utilisé avec d'autres classes dans le cadre modèle/vue.

Le modèle prend en charge les éléments modifiables, les en-têtes personnalisés et la possibilité d'insérer et de supprimer des lignes et des colonnes. Grâce à ces fonctionnalités, il est également possible d'insérer de nouveaux éléments enfants, comme le montre l'exemple de code correspondant.
Vue d'ensemble
Comme le décrit le document Model Subclassing Reference, les modèles doivent fournir des implémentations pour l'ensemble standard de fonctions de modèle : flags(), data(), headerData(), columnCount() et rowCount(). En outre, les modèles hiérarchiques, tels que celui-ci, doivent fournir des implémentations de index() et parent().
Un modèle modifiable doit fournir des implémentations de setData() et setHeaderData(), et doit renvoyer une combinaison appropriée de drapeaux à partir de sa fonction flags().
Étant donné que cet exemple permet de modifier les dimensions du modèle, nous devons également implémenter insertRows(), insertColumns(), removeRows() et removeColumns().
Conception
Comme dans l'exemple du modèle d'arbre simple, le modèle agit simplement comme une enveloppe autour d'une collection d'instances d'une classe TreeItem. Chaque TreeItem est conçu pour contenir les données d'une ligne d'éléments dans une vue arborescente, et contient donc une liste de valeurs correspondant aux données affichées dans chaque colonne.
Étant donné que QTreeView fournit une vue orientée ligne d'un modèle, il est naturel de choisir une conception orientée ligne pour les structures de données qui fourniront des données à ce type de vue par l'intermédiaire d'un modèle. Bien que cela rende le modèle d'arbre moins flexible et peut-être moins utile pour une utilisation avec des vues plus sophistiquées, cela le rend moins complexe à concevoir et plus facile à mettre en œuvre.
![]() | Relations entre les éléments internes Lors de la conception d'une structure de données à utiliser avec un modèle personnalisé, il est utile d'exposer le parent de chaque élément via une fonction comme TreeItem::parent(), car cela facilitera l'écriture de la fonction parent() propre au modèle. De même, une fonction comme TreeItem::child() est utile pour implémenter la fonction index() du modèle. Ainsi, chaque site Le diagramme montre comment les instances Dans l'exemple présenté, deux éléments de niveau supérieur, A et B, peuvent être obtenus à partir de l'élément racine en appelant sa fonction child(), et chacun de ces éléments renvoie le nœud racine à partir de leurs fonctions parent(), bien que cela ne soit montré que pour l'élément A. |
Chaque TreeItem stocke les données de chaque colonne de la ligne qu'il représente dans son membre privé itemData (une liste d'objets QVariant ). Comme il existe une correspondance univoque entre chaque colonne de la vue et chaque entrée de la liste, nous fournissons une simple fonction data() pour lire les entrées de la liste itemData et une fonction setData() pour permettre leur modification. Comme pour les autres fonctions de l'élément, cela simplifie la mise en œuvre des fonctions data() et setData() du modèle.
Nous plaçons un item à la racine de l'arbre des items. Cet élément racine correspond à l'index de modèle nul, QModelIndex(), qui est utilisé pour représenter le parent d'un élément de niveau supérieur lors de la gestion des index de modèle. Bien que l'élément racine n'ait pas de représentation visible dans les vues standard, nous utilisons sa liste interne d'objets QVariant pour stocker une liste de chaînes de caractères qui seront transmises aux vues pour être utilisées comme titres d'en-têtes horizontaux.
![]() | Accès aux données via le modèle Dans le cas illustré dans le diagramme, l'élément d'information représenté par a peut être obtenu à l'aide de l'API modèle/vue standard : QVariant a = model->index(0, 0, QModelIndex()).data(); Étant donné que chaque élément contient des données pour chaque colonne d'une ligne donnée, de nombreux index de modèle peuvent correspondre au même objet QVariant b = model->index(1, 0, QModelIndex()).data(); Le même site |
Dans la classe de modèle TreeModel, nous relions les objets TreeItem aux index de modèle en passant un pointeur pour chaque élément lorsque nous créons l'index de modèle correspondant avec QAbstractItemModel::createIndex() dans nos implémentations index() et parent(). Nous pouvons récupérer les pointeurs ainsi stockés en appelant la fonction internalPointer() sur l'index de modèle correspondant - nous créons notre propre fonction getItem() pour faire le travail à notre place, et nous l'appelons à partir de nos implémentations data() et parent().
Le stockage de pointeurs sur les éléments est pratique lorsque nous contrôlons la manière dont ils sont créés et détruits, car nous pouvons supposer qu'une adresse obtenue à partir de internalPointer() est un pointeur valide. Toutefois, certains modèles doivent gérer des éléments obtenus à partir d'autres composants d'un système et, dans de nombreux cas, il n'est pas possible de contrôler entièrement la manière dont les éléments sont créés ou détruits. Dans de telles situations, une approche purement basée sur les pointeurs doit être complétée par des sauvegardes afin de garantir que le modèle ne tente pas d'accéder à des éléments qui ont été supprimés.
| Stockage des informations dans la structure de données sous-jacente Plusieurs données sont stockées en tant qu'objets QVariant dans le membre Le diagramme montre comment les éléments d'information, représentés par les étiquettes a, b et c dans les deux diagrammes précédents, sont stockés dans les éléments A, B et C de la structure de données sous-jacente. Notez que les éléments d'information d'une même ligne du modèle sont tous obtenus à partir du même élément. Chaque élément d'une liste correspond à un élément d'information exposé par chaque colonne d'une ligne donnée du modèle. | ![]() |
Étant donné que l'implémentation de TreeModel a été conçue pour être utilisée avec QTreeView, nous avons ajouté une restriction sur la manière dont elle utilise les instances de TreeItem: chaque élément doit exposer le même nombre de colonnes de données. Cela rend la visualisation du modèle cohérente, en nous permettant d'utiliser l'élément racine pour déterminer le nombre de colonnes pour une ligne donnée, et n'ajoute que l'obligation de créer des éléments contenant suffisamment de données pour le nombre total de colonnes. Par conséquent, l'insertion et la suppression de colonnes sont des opérations qui prennent du temps, car nous devons parcourir l'arbre entier pour modifier chaque élément.
Une autre approche consisterait à concevoir la classe TreeModel de telle sorte qu'elle tronque ou développe la liste des données dans les instances TreeItem individuelles au fur et à mesure que les éléments de données sont modifiés. Toutefois, cette approche de redimensionnement "paresseuse" ne nous permettrait d'insérer et de supprimer des colonnes qu'à la fin de chaque ligne et ne permettrait pas d'insérer ou de supprimer des colonnes à des positions arbitraires dans chaque ligne.
![]() | Relier des éléments à l'aide d'index de modèle Comme dans l'exemple du modèle de l'arbre simple, le site Dans le diagramme, nous montrons comment l'implémentation parent() du modèle obtient l'index de modèle correspondant au parent d'un élément fourni par l'appelant, en utilisant les éléments montrés dans un diagramme précédent. Un pointeur sur l'élément C est obtenu à partir de l'index de modèle correspondant à l'aide de la fonction QModelIndex::internalPointer(). Le pointeur a été stocké en interne dans l'index lors de sa création. Étant donné que l'enfant contient un pointeur sur son parent, nous utilisons sa fonction parent() pour obtenir un pointeur sur l'élément B. L'index du modèle parent est créé à l'aide de la fonction QAbstractItemModel::createIndex(), en transmettant le pointeur sur l'élément B en tant que pointeur interne. |
Définition de la classe TreeItem
La classe TreeItem fournit des éléments simples qui contiennent plusieurs données, notamment des informations sur les éléments parents et enfants :
class TreeItem { public: explicit TreeItem(QVariantList data, TreeItem *parent = nullptr); TreeItem *child(int number); int childCount() const; int columnCount() const; QVariant data(int column) const; bool insertChildren(int position, int count, int columns); bool insertColumns(int position, int columns); TreeItem *parent(); bool removeChildren(int position, int count); bool removeColumns(int position, int columns); int row() const; bool setData(int column, const QVariant &value); private: std::vector<std::unique_ptr<TreeItem>> m_childItems; QVariantList itemData; TreeItem *m_parentItem; };
Nous avons conçu l'API de manière à ce qu'elle soit similaire à celle fournie par QAbstractItemModel en donnant à chaque élément des fonctions permettant de renvoyer le nombre de colonnes d'informations, de lire et d'écrire des données, et d'insérer et de supprimer des colonnes. Cependant, nous rendons la relation entre les éléments explicite en fournissant des fonctions pour traiter les "enfants" plutôt que les "lignes".
Chaque élément contient une liste de pointeurs vers les éléments enfants, un pointeur vers son élément parent et une liste d'objets QVariant correspondant aux informations contenues dans les colonnes d'une ligne donnée du modèle.
Mise en œuvre de la classe TreeItem
Chaque TreeItem est construit avec une liste de données et un élément parent facultatif :
TreeItem::TreeItem(QVariantList data, TreeItem *parent) : itemData(std::move(data)), m_parentItem(parent) {}
Au départ, chaque élément n'a pas d'enfants. Ceux-ci sont ajoutés au membre interne de l'élément childItems à l'aide de la fonction insertChildren() décrite plus loin.
Les enfants sont stockés dans std::unique_ptr pour garantir que chaque enfant ajouté à l'élément est supprimé lorsque l'élément lui-même est supprimé.
Comme chaque élément stocke un pointeur sur son parent, la fonction parent() est triviale :
TreeItem *TreeItem::parent() { return m_parentItem; }
Trois fonctions fournissent des informations sur les enfants d'un élément. child() renvoie un enfant spécifique à partir de la liste interne des enfants :
TreeItem *TreeItem::child(int number) { return (number >= 0 && number < childCount()) ? m_childItems.at(number).get() : nullptr; }
La fonction childCount() renvoie le nombre total d'enfants :
int TreeItem::childCount() const { return int(m_childItems.size()); }
La fonction row() est utilisée pour déterminer l'index de l'enfant dans la liste des enfants de son parent. Elle accède directement au membre childItems du parent pour obtenir cette information :
int TreeItem::row() const { if (!m_parentItem) return 0; const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(), [this](const std::unique_ptr<TreeItem> &treeItem) { return treeItem.get() == this; }); if (it != m_parentItem->m_childItems.cend()) return std::distance(m_parentItem->m_childItems.cbegin(), it); Q_ASSERT(false); // should not happen return -1; }
L'élément racine n'a pas d'élément parent ; pour cet élément, nous renvoyons zéro par souci de cohérence avec les autres éléments.
La fonction columnCount() renvoie simplement le nombre d'éléments de la liste interne itemData des objets QVariant:
int TreeItem::columnCount() const { return int(itemData.count()); }
Les données sont récupérées à l'aide de la fonction data(), qui accède à l'élément approprié de la liste itemData:
QVariant TreeItem::data(int column) const { return itemData.value(column); }
Les données sont définies à l'aide de la fonction setData(), qui ne stocke des valeurs dans la liste itemData que pour les index de liste valides, correspondant aux valeurs des colonnes du modèle :
bool TreeItem::setData(int column, const QVariant &value) { if (column < 0 || column >= itemData.size()) return false; itemData[column] = value; return true; }
Pour faciliter la mise en œuvre du modèle, nous renvoyons true pour indiquer que les données ont été définies avec succès.
Les modèles modifiables doivent souvent être redimensionnables, ce qui permet d'insérer et de supprimer des lignes et des colonnes. L'insertion de lignes sous un index de modèle donné dans le modèle entraîne l'insertion de nouveaux éléments enfants dans l'élément correspondant, gérée par la fonction insertChildren():
bool TreeItem::insertChildren(int position, int count, int columns) { if (position < 0 || position > qsizetype(m_childItems.size())) return false; for (int row = 0; row < count; ++row) { QVariantList data(columns); m_childItems.insert(m_childItems.cbegin() + position, std::make_unique<TreeItem>(data, this)); } return true; }
Cela garantit que les nouveaux éléments sont créés avec le nombre requis de colonnes et insérés à une position valide dans la liste interne childItems. Les éléments sont supprimés à l'aide de la fonction removeChildren():
bool TreeItem::removeChildren(int position, int count) { if (position < 0 || position + count > qsizetype(m_childItems.size())) return false; for (int row = 0; row < count; ++row) m_childItems.erase(m_childItems.cbegin() + position); return true; }
Comme nous l'avons vu plus haut, les fonctions d'insertion et de suppression de colonnes sont utilisées différemment de celles d'insertion et de suppression d'éléments enfants, car elles doivent être appelées sur chaque élément de l'arbre. Pour ce faire, nous appelons cette fonction de manière récursive sur chaque enfant de l'élément :
bool TreeItem::insertColumns(int position, int columns) { if (position < 0 || position > itemData.size()) return false; for (int column = 0; column < columns; ++column) itemData.insert(position, QVariant()); for (auto &child : std::as_const(m_childItems)) child->insertColumns(position, columns); return true; }
Définition de la classe TreeModel
La classe TreeModel fournit une implémentation de la classe QAbstractItemModel, exposant l'interface nécessaire pour un modèle qui peut être édité et redimensionné.
class TreeModel : public QAbstractItemModel { Q_OBJECT public: Q_DISABLE_COPY_MOVE(TreeModel) TreeModel(const QStringList &headers, const QString &data, QObject *parent = nullptr); ~TreeModel() override;
Le constructeur et le destructeur sont spécifiques à ce modèle.
QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = {}) const override; int columnCount(const QModelIndex &parent = {}) const override;
Les modèles d'arbres en lecture seule ne doivent fournir que les fonctions ci-dessus. Les fonctions publiques suivantes prennent en charge l'édition et le redimensionnement :
Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole) override; bool insertColumns(int position, int columns, const QModelIndex &parent = {}) override; bool removeColumns(int position, int columns, const QModelIndex &parent = {}) override; bool insertRows(int position, int rows, const QModelIndex &parent = {}) override; bool removeRows(int position, int rows, const QModelIndex &parent = {}) override; private: void setupModelData(const QList<QStringView> &lines); TreeItem *getItem(const QModelIndex &index) const; std::unique_ptr<TreeItem> rootItem; };
Pour simplifier cet exemple, les données exposées par le modèle sont organisées dans une structure de données par la fonction setupModelData() du modèle. De nombreux modèles réels ne traitent pas du tout les données brutes, mais travaillent simplement avec une structure de données existante ou une bibliothèque API.
Mise en œuvre de la classe TreeModel
Le constructeur crée un élément racine et l'initialise avec les données d'en-tête fournies :
TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent) : QAbstractItemModel(parent) { QVariantList rootData; for (const QString &header : headers) rootData << header; rootItem = std::make_unique<TreeItem>(rootData); setupModelData(QStringView{data}.split(u'\n')); }
Nous appelons la fonction interne setupModelData() pour convertir les données textuelles fournies en une structure de données que nous pouvons utiliser avec le modèle. D'autres modèles peuvent être initialisés avec une structure de données prête à l'emploi ou utiliser l'API d'une bibliothèque qui gère ses propres données.
TreeModel::~TreeModel() = default;
Le destructeur ne doit supprimer que l'élément racine, ce qui entraînera la suppression récursive de tous les éléments enfants. Cette opération est effectuée automatiquement par le destructeur par défaut, puisque l'élément racine est stocké dans un unique_ptr.
Puisque l'interface du modèle avec les autres composants du modèle/de la vue est basée sur les index du modèle, et que la structure interne des données est basée sur les éléments, de nombreuses fonctions implémentées par le modèle doivent être capables de convertir un index de modèle donné en son élément correspondant. Pour des raisons de commodité et de cohérence, nous avons défini une fonction getItem() pour effectuer cette tâche répétitive :
TreeItem *TreeModel::getItem(const QModelIndex &index) const { if (index.isValid()) { if (auto *item = static_cast<TreeItem*>(index.internalPointer())) return item; } return rootItem.get(); }
Chaque indice de modèle transmis à cette fonction doit correspondre à un élément valide de la mémoire. Si l'index n'est pas valide ou si son pointeur interne ne renvoie pas à un élément valide, l'élément racine est renvoyé à la place.
L'implémentation du modèle rowCount() est simple : elle utilise d'abord la fonction getItem() pour obtenir l'élément pertinent, puis renvoie le nombre d'enfants qu'il contient :
int TreeModel::rowCount(const QModelIndex &parent) const { if (parent.isValid() && parent.column() > 0) return 0; const TreeItem *parentItem = getItem(parent); return parentItem ? parentItem->childCount() : 0; }
En revanche, l'implémentation columnCount() n'a pas besoin de rechercher un élément particulier parce que tous les éléments sont définis pour avoir le même nombre de colonnes qui leur sont associées.
int TreeModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return rootItem->columnCount(); }
Par conséquent, le nombre de colonnes peut être obtenu directement à partir de l'élément racine.
Pour permettre l'édition et la sélection des éléments, la fonction flags() doit être implémentée de manière à renvoyer une combinaison de drapeaux comprenant les drapeaux Qt::ItemIsEditable et Qt::ItemIsSelectable ainsi que Qt::ItemIsEnabled:
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsEditable | QAbstractItemModel::flags(index); }
Le modèle doit pouvoir générer des index de modèle pour permettre à d'autres composants de demander des données et des informations sur sa structure. Cette tâche est effectuée par la fonction index(), qui est utilisée pour obtenir les index de modèle correspondant aux enfants d'un élément parent donné :
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid() && parent.column() != 0) return {};
Dans ce modèle, nous ne renvoyons les index de modèle pour les éléments enfants que si l'index parent est invalide (correspondant à l'élément racine) ou s'il a un numéro de colonne nul.
Nous utilisons la fonction personnalisée getItem() pour obtenir une instance TreeItem correspondant à l'index de modèle fourni et demander l'élément enfant correspondant à la ligne spécifiée.
TreeItem *parentItem = getItem(parent); if (!parentItem) return {}; if (auto *childItem = parentItem->child(row)) return createIndex(row, column, childItem); return {}; }
Étant donné que chaque élément contient des informations pour une ligne entière de données, nous créons un index de modèle pour l'identifier de manière unique en appelant la fonction createIndex() avec les numéros de ligne et de colonne et un pointeur sur l'élément. Dans la fonction data(), nous utiliserons le pointeur de l'élément et le numéro de la colonne pour accéder aux données associées à l'index du modèle ; dans ce modèle, le numéro de ligne n'est pas nécessaire pour identifier les données.
La fonction parent() fournit des index de modèle pour les parents des éléments en trouvant l'élément correspondant à un index de modèle donné, en utilisant sa fonction parent() pour obtenir l'élément parent, puis en créant un index de modèle pour représenter le parent. (Voir le diagramme ci-dessus).
QModelIndex TreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return {}; TreeItem *childItem = getItem(index); TreeItem *parentItem = childItem ? childItem->parent() : nullptr; return (parentItem != rootItem.get() && parentItem != nullptr) ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{}; }
Les éléments sans parents, y compris l'élément racine, sont traités en renvoyant un index de modèle nul. Sinon, un index de modèle est créé et renvoyé comme dans la fonction index(), avec un numéro de ligne approprié, mais avec un numéro de colonne nul pour être cohérent avec le schéma utilisé dans l'implémentation de index().
Test du modèle
L'implémentation correcte d'un modèle d'élément peut s'avérer difficile. La classe QAbstractItemModelTester du module Qt Test vérifie la cohérence du modèle, comme la création de l'index du modèle et les relations parent-enfant.
Vous pouvez tester votre modèle en passant une instance de modèle au constructeur de la classe, par exemple dans le cadre d'un test unitaire Qt Test :
class TestEditableTreeModel : public QObject { Q_OBJECTprivate slots: void testTreeModel() ; } ;void TestEditableTreeModel::testTreeModel() { constexpr auto fileName = ":/default.txt"_L1 ; QFile file(fileName) ; QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), qPrintable(fileName + " cannot be opened: "_L1 + file.errorString())); const QStringList headers{"column1"_L1, "column2"_L1} ; TreeModel model(headers, QString::fromUtf8(file.readAll())) ; QAbstractItemModelTester tester(&model) ; } QTEST_APPLESS_MAIN(TestEditableTreeModel)#include "test.moc"
Pour créer un test qui peut être exécuté à l'aide de l'exécutable ctest, on utilise add_test():
# Unit Test
include(CTest)
qt_add_executable(editabletreemodel_tester
test.cpp
treeitem.cpp treeitem.h
treemodel.cpp treemodel.h)
target_link_libraries(editabletreemodel_tester PRIVATE
Qt6::Core
Qt6::Test
)
if(ANDROID)
target_link_libraries(editabletreemodel_tester PRIVATE
Qt6::Gui
)
endif()
qt_add_resources(editabletreemodel_tester "editabletreemodel_tester"
PREFIX
"/"
FILES
${editabletreemodel_resource_files}
)
add_test(NAME editabletreemodel_tester
COMMAND editabletreemodel_tester)© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.



