Exemple de modèle d'arbre simple
L'exemple de modèle d'arbre simple montre comment utiliser un modèle hiérarchique avec les classes de vues standard de Qt.

L'architecture modèle/vue de Qt offre aux vues un moyen standard de manipuler les informations d'une source de données, en utilisant un modèle abstrait des données pour simplifier et normaliser la manière dont on y accède. Les modèles simples représentent les données sous la forme d'un tableau d'éléments et permettent aux vues d'accéder à ces données par le biais d'un système basé sur un index. Plus généralement, les modèles peuvent être utilisés pour représenter les données sous la forme d'une structure arborescente en permettant à chaque élément d'agir en tant que parent d'une table d'éléments enfants.
Avant d'essayer de mettre en œuvre un modèle arborescent, il convient de se demander si les données sont fournies par une source externe ou si elles seront gérées dans le modèle lui-même. Dans cet exemple, nous mettrons en œuvre une structure interne pour contenir les données plutôt que de discuter de la manière d'empaqueter les données provenant d'une source externe.
Conception et concepts
La structure de données que nous utilisons pour représenter la structure des données prend la forme d'un arbre construit à partir d'objets TreeItem. Chaque objet TreeItem représente un élément dans une vue arborescente et contient plusieurs colonnes de données.
| Structure simple du modèle arborescent Les données sont stockées en interne dans le modèle à l'aide d'objets Chaque Étant donné que chaque élément d'une arborescence contient généralement plusieurs colonnes de données (un titre et un résumé dans cet exemple), il est naturel de stocker ces informations dans chaque élément. Par souci de simplicité, nous utiliserons une liste d'objets QVariant pour stocker les données de chaque colonne de l'élément. |
L'utilisation d'une structure arborescente basée sur des pointeurs signifie que, lors du passage d'un index de modèle à une vue, nous pouvons enregistrer l'adresse de l'élément correspondant dans l'index (voir QAbstractItemModel::createIndex()) et la récupérer plus tard avec QModelIndex::internalPointer(). Cela facilite l'écriture du modèle et garantit que tous les index de modèle qui font référence au même élément ont le même pointeur de données interne.
Avec la structure de données appropriée en place, nous pouvons créer un modèle d'arbre avec un minimum de code supplémentaire pour fournir des index de modèle et des données à d'autres composants.
Définition de la classe TreeItem
La classe TreeItem est définie comme suit :
class TreeItem { public: explicit TreeItem(QVariantList data, TreeItem *parentItem = nullptr); void appendChild(std::unique_ptr<TreeItem> &&child); TreeItem *child(int row); int childCount() const; int columnCount() const; QVariant data(int column) const; int row() const; TreeItem *parentItem(); private: std::vector<std::unique_ptr<TreeItem>> m_childItems; QVariantList m_itemData; TreeItem *m_parentItem; };
La classe est une classe C++ de base. Elle n'hérite pas de QObject et ne fournit pas de signaux ni d'emplacements. Elle est utilisée pour contenir une liste de QVariants, contenant des données de colonne et des informations sur sa position dans l'arborescence. Les fonctions offrent les caractéristiques suivantes :
- La fonction
appendChildItem()est utilisée pour ajouter des données lors de la construction initiale du modèle et n'est pas utilisée dans le cadre d'une utilisation normale. - Les fonctions
child()etchildCount()permettent au modèle d'obtenir des informations sur tous les éléments enfants. - L'information sur le nombre de colonnes associées à l'élément est fournie par
columnCount(), et les données de chaque colonne peuvent être obtenues à l'aide de la fonction data(). - Les fonctions
row()etparent()sont utilisées pour obtenir le numéro de ligne de l'élément et l'élément parent.
Les données relatives à l'élément parent et à la colonne sont stockées dans les variables membres privées parentItem et itemData. La variable childItems contient une liste de pointeurs vers les éléments enfants de l'élément.
Mise en œuvre de la classe TreeItem
Le constructeur n'est utilisé que pour enregistrer le parent de l'élément et les données associées à chaque colonne.
TreeItem::TreeItem(QVariantList data, TreeItem *parent) : m_itemData(std::move(data)), m_parentItem(parent) {}
Un pointeur sur chacun des éléments enfants appartenant à cet élément sera stocké dans la variable membre privée childItems sous la forme d'un std::unique_ptr. Lorsque le destructeur de la classe est appelé, les éléments enfants sont automatiquement supprimés afin de garantir la réutilisation de leur mémoire :
Étant donné que chaque élément enfant est construit lorsque le modèle est initialement alimenté en données, la fonction permettant d'ajouter des éléments enfants est simple :
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child) { m_childItems.push_back(std::move(child)); }
Chaque élément est capable de renvoyer n'importe lequel de ses éléments enfants lorsqu'on lui donne un numéro de ligne approprié. Par exemple, dans le diagramme ci-dessus, l'élément marqué de la lettre "A" correspond à l'enfant de l'élément racine avec row = 0, l'élément "B" est un enfant de l'élément "A" avec row = 1, et l'élément "C" est un enfant de l'élément racine avec row = 1.
La fonction child() renvoie l'enfant qui correspond au numéro de ligne spécifié dans la liste des éléments enfants de l'élément :
TreeItem *TreeItem::child(int row) { return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr; }
Le nombre d'éléments enfants détenus peut être trouvé avec childCount():
int TreeItem::childCount() const { return int(m_childItems.size()); }
Le site TreeModel utilise cette fonction pour déterminer le nombre de lignes qui existent pour un élément parent donné.
La fonction row() indique l'emplacement de l'élément dans la liste des éléments de son parent :
int TreeItem::row() const { if (m_parentItem == nullptr) 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; }
Notez que, bien que l'élément racine (sans élément parent) se voit automatiquement attribuer un numéro de ligne de 0, cette information n'est jamais utilisée par le modèle.
Le nombre de colonnes de données dans l'élément est trivialement renvoyé par la fonction columnCount().
int TreeItem::columnCount() const { return int(m_itemData.count()); }
Les données relatives aux colonnes sont renvoyées par la fonction data(). Nous utilisons la fonction de commodité QList::value() qui vérifie les limites et renvoie une construction par défaut QVariant en cas de violation :
QVariant TreeItem::data(int column) const { return m_itemData.value(column); }
Le parent de l'élément est trouvé avec parent():
TreeItem *TreeItem::parentItem() { return m_parentItem; }
Notez que, puisque l'élément racine du modèle n'aura pas de parent, cette fonction renverra zéro dans ce cas. Nous devons nous assurer que le modèle gère correctement ce cas lorsque nous implémentons la fonction TreeModel::parent().
Définition de la classe TreeModel
La classe TreeModel est définie comme suit :
class TreeModel : public QAbstractItemModel { Q_OBJECT public: Q_DISABLE_COPY_MOVE(TreeModel) explicit TreeModel(const QString &data, QObject *parent = nullptr); ~TreeModel() override; QVariant data(const QModelIndex &index, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) 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; private: static void setupModelData(const QList<QStringView> &lines, TreeItem *parent); std::unique_ptr<TreeItem> rootItem; };
Cette classe est similaire à la plupart des autres sous-classes de QAbstractItemModel qui fournissent des modèles en lecture seule. Seule la forme du constructeur et la fonction setupModelData() sont spécifiques à ce modèle. En outre, nous fournissons un destructeur pour nettoyer le modèle lorsqu'il est détruit.
Mise en œuvre de la classe TreeModel
Pour des raisons de simplicité, le modèle ne permet pas de modifier ses données. Par conséquent, le constructeur prend un argument contenant les données que le modèle partagera avec les vues et les délégués :
TreeModel::TreeModel(const QString &data, QObject *parent) : QAbstractItemModel(parent) , rootItem(std::make_unique<TreeItem>(QVariantList{tr("Title"), tr("Summary")})) { setupModelData(QStringView{data}.split(u'\n'), rootItem.get()); }
Il appartient au constructeur de créer un élément racine pour le modèle. Cet élément ne contient que des données d'en-tête verticales pour des raisons de commodité. Nous l'utilisons également pour référencer la structure de données interne qui contient les données du modèle, et il est utilisé pour représenter un parent imaginaire des éléments de haut niveau dans le modèle. L'élément racine est géré avec un std::unique_ptr pour s'assurer que l'arbre entier d'éléments est supprimé lorsque le modèle est supprimé.
La structure de données interne du modèle est remplie d'éléments par la fonction setupModelData(). Nous examinerons cette fonction séparément à la fin de ce document.
Le destructeur s'assure que l'élément racine et tous ses descendants sont supprimés lorsque le modèle est détruit. Cela se fait automatiquement puisque l'élément racine est stocké dans un unique_ptr.
TreeModel::~TreeModel() = default;
Comme nous ne pouvons pas ajouter de données au modèle une fois qu'il est construit et configuré, cela simplifie la gestion de l'arbre interne des éléments.
Les modèles doivent implémenter une fonction index() afin de fournir des index que les vues et les délégués utiliseront pour accéder aux données. Les index sont créés pour d'autres composants lorsqu'ils sont référencés par leurs numéros de ligne et de colonne et par l'index de leur modèle parent. Si un index de modèle invalide est spécifié comme parent, c'est au modèle de renvoyer un index correspondant à un élément de premier niveau dans le modèle.
Lorsqu'un index de modèle est fourni, nous vérifions d'abord s'il est valide. Dans le cas contraire, nous obtenons le pointeur de données de l'index du modèle à l'aide de la fonction internalPointer() et l'utilisons pour référencer un objet TreeItem. Notez que tous les index de modèle que nous construisons contiendront un pointeur vers un objet TreeItem existant, de sorte que nous pouvons garantir que tous les index de modèle valides que nous recevons contiendront un pointeur de données valide.
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return {}; TreeItem *parentItem = parent.isValid() ? static_cast<TreeItem*>(parent.internalPointer()) : rootItem.get(); if (auto *childItem = parentItem->child(row)) return createIndex(row, column, childItem); return {}; }
Étant donné que les arguments ligne et colonne de cette fonction font référence à un élément enfant de l'élément parent correspondant, nous obtenons l'élément à l'aide de la fonction TreeItem::child(). La fonction createIndex() est utilisée pour créer un index de modèle à renvoyer. Nous spécifions les numéros de ligne et de colonne, ainsi qu'un pointeur sur l'élément lui-même. L'index de modèle peut être utilisé ultérieurement pour obtenir les données de l'élément.
La façon dont les objets TreeItem sont définis facilite l'écriture de la fonction parent():
QModelIndex TreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return {}; auto *childItem = static_cast<TreeItem*>(index.internalPointer()); TreeItem *parentItem = childItem->parentItem(); return parentItem != rootItem.get() ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{}; }
Il suffit de s'assurer que nous ne renvoyons jamais un index de modèle correspondant à l'élément racine. Pour être cohérent avec la manière dont la fonction index() est implémentée, nous renvoyons un index de modèle invalide pour le parent de tout élément de premier niveau dans le modèle.
Lors de la création d'un index de modèle à renvoyer, nous devons spécifier les numéros de ligne et de colonne de l'élément parent au sein de son propre parent. Nous pouvons facilement découvrir le numéro de ligne avec la fonction TreeItem::row(), mais nous suivons une convention qui consiste à spécifier 0 comme numéro de colonne du parent. L'index du modèle est créé avec createIndex() de la même manière que dans la fonction index().
La fonction rowCount() renvoie simplement le nombre d'éléments enfants pour TreeItem qui correspond à un index de modèle donné, ou le nombre d'éléments de niveau supérieur si un index invalide est spécifié :
int TreeModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; const TreeItem *parentItem = parent.isValid() ? static_cast<const TreeItem*>(parent.internalPointer()) : rootItem.get(); return parentItem->childCount(); }
Étant donné que chaque élément gère ses propres données de colonne, la fonction columnCount() doit appeler la fonction columnCount() de l'élément pour déterminer le nombre de colonnes présentes pour un indice de modèle donné. Comme pour la fonction rowCount(), si un index de modèle non valide est spécifié, le nombre de colonnes renvoyées est déterminé à partir de l'élément racine :
int TreeModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return static_cast<TreeItem*>(parent.internalPointer())->columnCount(); return rootItem->columnCount(); }
Les données sont obtenues à partir du modèle via data(). Étant donné que l'élément gère ses propres colonnes, nous devons utiliser le numéro de colonne pour récupérer les données avec la fonction TreeItem::data():
QVariant TreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || role != Qt::DisplayRole) return {}; const auto *item = static_cast<const TreeItem*>(index.internalPointer()); return item->data(index.column()); }
Notez que nous ne prenons en charge que DisplayRole dans cette implémentation, et que nous renvoyons également des objets QVariant invalides pour les index de modèle invalides.
Nous utilisons la fonction flags() pour nous assurer que les vues savent que le modèle est en lecture seule :
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { return index.isValid() ? QAbstractItemModel::flags(index) : Qt::ItemFlags(Qt::NoItemFlags); }
La fonction headerData() renvoie les données que nous avons commodément stockées dans l'élément racine :
QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const { return orientation == Qt::Horizontal && role == Qt::DisplayRole ? rootItem->data(section) : QVariant{}; }
Ces informations auraient pu être fournies d'une autre manière : soit spécifiées dans le constructeur, soit codées en dur dans la fonction headerData().
Mise en place des données dans le modèle
Nous utilisons la fonction setupModelData() pour définir les données initiales du modèle. Cette fonction analyse un fichier texte, extrait les chaînes de texte à utiliser dans le modèle et crée des objets item qui enregistrent à la fois les données et la structure globale du modèle. Naturellement, cette fonction fonctionne d'une manière très spécifique à ce modèle. Nous décrivons ci-après son comportement et renvoyons le lecteur au code de l'exemple pour plus d'informations.
Nous commençons par un fichier texte au format suivant :
Getting Started How to familiarize yourself with Qt Widgets Designer Launching Designer Running the Qt Widgets Designer application The User Interface How to interact with Qt Widgets Designer ...
Connection Editing Mode Connecting widgets together with signals and slots Connecting Objects Making connections in Qt Widgets Designer Editing Connections Changing existing connections
Nous traitons le fichier texte selon les deux règles suivantes :
- Pour chaque paire de chaînes sur chaque ligne, nous créons un élément (ou nœud) dans une structure arborescente et plaçons chaque chaîne dans une colonne de données de l'élément.
- Lorsque la première chaîne d'une ligne est indentée par rapport à la première chaîne de la ligne précédente, l'élément devient un enfant de l'élément créé précédemment.
Pour garantir le bon fonctionnement du modèle, il suffit de créer des instances de TreeItem avec les données et l'élément parent corrects.
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 TestSimpleTreeModel : public QObject { Q_OBJECTprivate slots: void testTreeModel() ; } ;void TestSimpleTreeModel::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())); Modèle d'arbre modèle(QString::fromUtf8(file.readAll())) ; QAbstractItemModelTester tester(&model) ; } QTEST_APPLESS_MAIN(TestSimpleTreeModel)#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(simpletreemodel_tester
test.cpp
treeitem.cpp treeitem.h
treemodel.cpp treemodel.h)
target_link_libraries(simpletreemodel_tester PRIVATE
Qt6::Core
Qt6::Test
)
if(ANDROID)
target_link_libraries(simpletreemodel_tester PRIVATE
Qt6::Gui
)
endif()
qt_add_resources(simpletreemodel_tester "simpletreemodel_tester"
PREFIX
"/"
FILES
${simpletreemodel_resource_files}
)
add_test(NAME simpletreemodel_tester
COMMAND simpletreemodel_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.