Beispiel für ein einfaches Baummodell
Das Beispiel „Einfaches Baummodell“ zeigt, wie ein hierarchisches Modell mit den Standard-Ansichtsklassen von Qt verwendet wird.

Die Modell-Ansicht-Architektur von Qt bietet eine Standardmethode, mit der Ansichten Informationen in einer Datenquelle bearbeiten können, wobei ein abstraktes Modell der Daten verwendet wird, um den Zugriff darauf zu vereinfachen und zu standardisieren. Einfache Modelle stellen Daten als Tabelle mit Elementen dar und ermöglichen es Ansichten, über ein indexbasiertes System auf diese Daten zuzugreifen. Allgemeiner gesagt können Modelle dazu verwendet werden, Daten in Form einer Baumstruktur darzustellen, indem jedes Element als übergeordnetes Element einer Tabelle mit untergeordneten Elementen fungiert.
Bevor man versucht, ein Baummodell zu implementieren, sollte man überlegen, ob die Daten von einer externen Quelle bereitgestellt werden oder ob sie innerhalb des Modells selbst verwaltet werden sollen. In diesem Beispiel werden wir eine interne Struktur zur Speicherung von Daten implementieren, anstatt zu erörtern, wie Daten aus einer externen Quelle eingebunden werden können.
Entwurf und Konzepte
Die Datenstruktur, die wir zur Darstellung der Datenstruktur verwenden, hat die Form eines Baums, der aus „ TreeItem “-Objekten aufgebaut ist. Jedes „ TreeItem “-Objekt repräsentiert ein Element in einer Baumansicht und enthält mehrere Datenspalten.
| Einfache Baumstruktur Die Daten werden intern im Modell mithilfe von „ Jedes „ Da jedes Element in einer Baumansicht in der Regel mehrere Datenspalten enthält (in diesem Beispiel einen Titel und eine Zusammenfassung), ist es naheliegend, diese Informationen in jedem Element zu speichern. Der Einfachheit halber verwenden wir eine Liste von „ QVariant “-Objekten, um die Daten für jede Spalte im Element zu speichern. |
Die Verwendung einer zeigerbasierten Baumstruktur bedeutet, dass wir bei der Übergabe eines Modellindexes an eine Ansicht die Adresse des entsprechenden Elements im Index speichern können (siehe ` QAbstractItemModel::createIndex()`) und diese später mit ` QModelIndex::internalPointer()` wieder abrufen können. Dies vereinfacht das Schreiben des Modells und stellt sicher, dass alle Modellindizes, die auf dasselbe Element verweisen, denselben internen Datenzeiger haben.
Mit der entsprechenden Datenstruktur können wir ein Baummodell mit einem minimalen Aufwand an zusätzlichem Code erstellen, um anderen Komponenten Modellindizes und Daten zur Verfügung zu stellen.
Definition der Klasse `TreeItem`
Die Klasse „ TreeItem “ ist wie folgt definiert:
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;
};Die Klasse ist eine einfache C++-Klasse. Sie erbt nicht von „ QObject “ und stellt keine Signale und Slots bereit. Sie dient dazu, eine Liste von QVariants zu speichern, die Spaltendaten sowie Informationen über deren Position in der Baumstruktur enthalten. Die Funktionen bieten die folgenden Funktionen:
- Die Klasse „
appendChildItem()“ wird zum Hinzufügen von Daten beim erstmaligen Aufbau des Modells verwendet und kommt im normalen Betrieb nicht zum Einsatz. - Die Funktionen `
child()` und `childCount()` ermöglichen es dem Modell, Informationen über beliebige untergeordnete Elemente abzurufen. - Informationen über die Anzahl der mit dem Element verbundenen Spalten liefert die Funktion `
columnCount()`, und die Daten in jeder Spalte können mit der Funktion `data()` abgerufen werden. - Die Funktionen „
row()“ und „parent()“ dienen dazu, die Zeilennummer und das übergeordnete Element des Elements abzurufen.
Das übergeordnete Element und die Spaltendaten werden in den privaten Membervariablen „ parentItem “ und „ itemData “ gespeichert. Die Variable „ childItems “ enthält eine Liste von Zeigern auf die eigenen untergeordneten Elemente des Elements.
Implementierung der Klasse „TreeItem“
Der Konstruktor dient lediglich dazu, das übergeordnete Element des Elements sowie die mit jeder Spalte verbundenen Daten zu erfassen.
TreeItem::TreeItem(QVariantList data, TreeItem *parent)
: m_itemData(std::move(data)), m_parentItem(parent)
{}Ein Zeiger auf jedes der zu diesem Element gehörenden untergeordneten Elemente wird in der privaten Membervariablen „ childItems “ als `std::unique_ptr` gespeichert. Wenn der Destruktor der Klasse aufgerufen wird, werden die untergeordneten Elemente automatisch gelöscht, um sicherzustellen, dass ihr Speicher wiederverwendet wird:
Da jedes der untergeordneten Elemente beim ersten Befüllen des Modells mit Daten instanziiert wird, ist die Funktion zum Hinzufügen von untergeordneten Elementen unkompliziert:
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child)
{
m_childItems.push_back(std::move(child));
}Jedes Element kann bei Angabe einer entsprechenden Zeilennummer eines seiner untergeordneten Elemente zurückgeben. Im obigen Diagramm entspricht beispielsweise das mit dem Buchstaben „A“ gekennzeichnete Element dem untergeordneten Element des Stammelements mit der ID „ row = 0 “, das Element „B“ ist ein untergeordnetes Element des Elements „A“ mit der ID „ row = 1 “ und das Element „C“ ist ein untergeordnetes Element des Stammelements mit der ID „ row = 1 “.
Die Funktion „ child() “ gibt das Kind zurück, das der angegebenen Zeilennummer in der Liste der Kinderelemente des Elements entspricht:
TreeItem *TreeItem::child(int row)
{
return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr;
}Die Anzahl der vorhandenen untergeordneten Elemente lässt sich mit ` childCount()` ermitteln:
int TreeItem::childCount() const
{
return int(m_childItems.size());
}TreeModel verwendet diese Funktion, um die Anzahl der Zeilen zu ermitteln, die für ein bestimmtes übergeordnetes Element vorhanden sind.
Die Funktion „ row() “ gibt die Position des Elements innerhalb der Elementliste seines übergeordneten Elements an:
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;
}Beachten Sie, dass dem Stammelement (ohne übergeordnetes Element) zwar automatisch die Zeilennummer 0 zugewiesen wird, diese Information vom Modell jedoch niemals verwendet wird.
Die Anzahl der Datenspalten im Element wird von der Funktion ` columnCount() ` ganz einfach zurückgegeben.
int TreeItem::columnCount() const
{
return int(m_itemData.count());
}Spaltendaten werden von der Funktion ` data() ` zurückgegeben. Wir verwenden die Hilfsfunktion ` QList::value()`, die die Grenzen überprüft und bei deren Überschreitung ein standardmäßig konstruiertes ` QVariant ` zurückgibt:
QVariant TreeItem::data(int column) const
{
return m_itemData.value(column);
}Das übergeordnete Element des Objekts wird mit „ parent() “ ermittelt:
TreeItem *TreeItem::parentItem()
{
return m_parentItem;
}Beachten Sie, dass das Stammelement im Modell kein übergeordnetes Element hat und diese Funktion in diesem Fall daher den Wert Null zurückgibt. Wir müssen sicherstellen, dass das Modell diesen Fall korrekt behandelt, wenn wir die Funktion ` TreeModel::parent() ` implementieren.
Definition der Klasse „TreeModel“
Die Klasse „ TreeModel “ ist wie folgt definiert:
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;
};Diese Klasse ähnelt den meisten anderen Unterklassen von „ QAbstractItemModel “, die schreibgeschützte Modelle bereitstellen. Lediglich die Form des Konstruktors und die Funktion „ setupModelData() “ sind spezifisch für dieses Modell. Darüber hinaus stellen wir einen Destruktor bereit, der bei der Zerstörung des Modells für die Bereinigung sorgt.
Implementierung der Klasse `TreeModel`
Der Einfachheit halber erlaubt das Modell keine Bearbeitung seiner Daten. Daher nimmt der Konstruktor ein Argument entgegen, das die Daten enthält, die das Modell an Ansichten und Delegaten weitergibt:
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());
}Es ist Aufgabe des Konstruktors, ein Stammelement für das Modell zu erstellen. Dieses Element enthält der Einfachheit halber nur vertikale Kopfzeilendaten. Wir nutzen es außerdem, um auf die interne Datenstruktur zu verweisen, die die Modelldaten enthält, und es dient dazu, einen imaginären übergeordneten Eintrag für Elemente der obersten Ebene im Modell darzustellen. Das Stammelement wird mit einem `std::unique_ptr` verwaltet, um sicherzustellen, dass der gesamte Elementbaum gelöscht wird, wenn das Modell gelöscht wird.
Die interne Datenstruktur des Modells wird durch die Funktion ` setupModelData() ` mit Elementen gefüllt. Wir werden diese Funktion am Ende dieses Dokuments gesondert betrachten.
Der Destruktor stellt sicher, dass das Stammelement und alle seine Nachkommen gelöscht werden, wenn das Modell zerstört wird. Dies geschieht automatisch, da das Stammelement in einem `unique_ptr` gespeichert ist.
TreeModel::~TreeModel() = default;Da wir dem Modell nach seiner Erstellung und Einrichtung keine Daten mehr hinzufügen können, vereinfacht dies die Verwaltung des internen Elementsbaums.
Modelle müssen eine Funktion ` index() ` implementieren, um Indizes bereitzustellen, die von Ansichten und Delegaten beim Zugriff auf Daten verwendet werden können. Indizes werden für andere Komponenten erstellt, wenn diese über ihre Zeilen- und Spaltennummern sowie den Index ihres übergeordneten Modells referenziert werden. Wird ein ungültiger Modellindex als übergeordnetes Element angegeben, ist es Aufgabe des Modells, einen Index zurückzugeben, der einem Element der obersten Ebene im Modell entspricht.
Wird ein Modellindex übergeben, prüfen wir zunächst, ob dieser gültig ist. Ist dies nicht der Fall, gehen wir davon aus, dass auf ein Element der obersten Ebene verwiesen wird; andernfalls ermitteln wir den Datenzeiger aus dem Modellindex mithilfe seiner Funktion ` internalPointer()` und verwenden ihn, um auf ein ` TreeItem `-Objekt zu verweisen. Beachten Sie, dass alle von uns erstellten Modellindizes einen Zeiger auf ein vorhandenes ` TreeItem` enthalten, sodass wir garantieren können, dass jeder gültige Modellindex, den wir erhalten, einen gültigen Datenzeiger enthält.
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 {};
}Da sich die Argumente „row“ und „column“ dieser Funktion auf ein untergeordnetes Element des entsprechenden übergeordneten Elements beziehen, rufen wir das Element mithilfe der Funktion „ TreeItem::child() “ ab. Die Funktion „ createIndex()“ wird verwendet, um einen Modellindex zu erstellen, der zurückgegeben werden soll. Wir geben die Zeilen- und Spaltennummern sowie einen Zeiger auf das Element selbst an. Der Modellindex kann später verwendet werden, um die Daten des Elements abzurufen.
Die Art und Weise, wie die „ TreeItem “-Objekte definiert sind, erleichtert das Schreiben der Funktion „ 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{};
}Wir müssen lediglich sicherstellen, dass wir niemals einen Modellindex zurückgeben, der dem Stammelement entspricht. Um mit der Implementierung der Funktion ` index() ` konsistent zu bleiben, geben wir für das übergeordnete Element aller Elemente der obersten Ebene im Modell einen ungültigen Modellindex zurück.
Beim Erstellen eines zurückzugebenden Modellindexes müssen wir die Zeilen- und Spaltennummern des übergeordneten Elements innerhalb seines eigenen übergeordneten Elements angeben. Die Zeilennummer lässt sich leicht mit der Funktion ` TreeItem::row() ` ermitteln, aber wir folgen der Konvention, als Spaltennummer des übergeordneten Elements den Wert 0 anzugeben. Der Modellindex wird mit ` createIndex()` auf dieselbe Weise erstellt wie in der Funktion ` index() `.
Die Funktion ` rowCount() ` gibt einfach die Anzahl der untergeordneten Elemente für den ` TreeItem ` zurück, der einem bestimmten Modellindex entspricht, oder die Anzahl der Elemente der obersten Ebene, falls ein ungültiger Index angegeben wird:
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();
}Da jedes Element seine eigenen Spaltendaten verwaltet, muss die Funktion ` columnCount() ` die eigene Funktion ` columnCount() ` des Elements aufrufen, um zu ermitteln, wie viele Spalten für einen bestimmten Modellindex vorhanden sind. Wie bei der Funktion ` rowCount() ` wird die Anzahl der zurückgegebenen Spalten aus dem Stammelement ermittelt, wenn ein ungültiger Modellindex angegeben wird:
int TreeModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return static_cast<TreeItem*>(parent.internalPointer())->columnCount();
return rootItem->columnCount();
}Die Daten werden über „ data() “ aus dem Modell abgerufen. Da das Element seine eigenen Spalten verwaltet, müssen wir die Spaltennummer verwenden, um die Daten mit der Funktion „ TreeItem::data() “ abzurufen:
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());
}Beachten Sie, dass wir in dieser Implementierung nur die Funktion „ DisplayRole “ unterstützen und bei ungültigen Modellindizes ebenfalls ungültige „ QVariant “-Objekte zurückgeben.
Wir verwenden die Funktion ` flags() `, um sicherzustellen, dass Views erkennen, dass das Modell schreibgeschützt ist:
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
return index.isValid()
? QAbstractItemModel::flags(index) : Qt::ItemFlags(Qt::NoItemFlags);
}Die Funktion „ headerData() “ gibt Daten zurück, die wir praktischerweise im Stammelement gespeichert haben:
QVariant TreeModel::headerData(int section, Qt::Orientation orientation,
int role) const
{
return orientation == Qt::Horizontal && role == Qt::DisplayRole
? rootItem->data(section) : QVariant{};
}Diese Informationen hätten auch auf andere Weise bereitgestellt werden können: entweder im Konstruktor angegeben oder fest in die Funktion ` headerData() ` eingebettet.
Einrichten der Daten im Modell
Wir verwenden die Funktion „ setupModelData() “, um die Anfangsdaten im Modell einzurichten. Diese Funktion analysiert eine Textdatei, extrahiert Textzeichenfolgen zur Verwendung im Modell und erstellt Elementobjekte, die sowohl die Daten als auch die gesamte Modellstruktur erfassen. Natürlich funktioniert diese Funktion auf eine Weise, die sehr spezifisch für dieses Modell ist. Wir geben im Folgenden eine Beschreibung ihres Verhaltens und verweisen den Leser für weitere Informationen auf den Beispielcode selbst.
Wir beginnen mit einer Textdatei im folgenden Format:
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 connectionsWir verarbeiten die Textdatei anhand der folgenden zwei Regeln:
- Für jedes Paar von Zeichenfolgen in jeder Zeile wird ein Element (oder Knoten) in einer Baumstruktur erstellt, und jede Zeichenfolge wird in eine Datenspalte des Elements eingefügt.
- Wenn die erste Zeichenkette einer Zeile gegenüber der ersten Zeichenkette der vorherigen Zeile eingerückt ist, wird das Element als untergeordnetes Element des zuvor erstellten Elements angelegt.
Um sicherzustellen, dass das Modell korrekt funktioniert, müssen lediglich Instanzen von „ TreeItem “ mit den richtigen Daten und dem richtigen übergeordneten Element erstellt werden.
Testen des Modells
Die korrekte Implementierung eines Elementmodells kann eine Herausforderung darstellen. Die Klasse ` QAbstractItemModelTester ` aus dem Qt Test Moduls prüft die Konsistenz des Modells, beispielsweise die Erstellung des Modellindexes und die Eltern-Kind-Beziehungen.
Sie können Ihr Modell testen, indem Sie einfach eine Modellinstanz an den Konstruktor der Klasse übergeben, beispielsweise im Rahmen eines Qt-Unit-Tests:
class TestSimpleTreeModel : public QObject
{
Q_OBJECT
private 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()));
TreeModel model(QString::fromUtf8(file.readAll()));
QAbstractItemModelTester tester(&model);
}
QTEST_APPLESS_MAIN(TestSimpleTreeModel)
#include "test.moc"Um einen Test zu erstellen, der mit dem Programm „ ctest “ ausgeführt werden kann, wird „ add_test() “ verwendet:
# 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.