Beispiel für ein einfaches Baummodell
Das Simple Tree Model Beispiel zeigt, wie man ein hierarchisches Modell mit den Standard-View-Klassen von Qt verwenden kann.
Die Model/View-Architektur von Qt bietet einen Standardweg für Views, um Informationen in einer Datenquelle zu manipulieren, wobei ein abstraktes Modell der Daten verwendet wird, um die Art des Zugriffs zu vereinfachen und zu standardisieren. Einfache Modelle stellen die Daten als eine Tabelle von Elementen dar und ermöglichen es den Ansichten, auf diese Daten über ein indexbasiertes System zuzugreifen. Ganz allgemein können Modelle verwendet werden, um Daten in Form einer Baumstruktur darzustellen, wobei jedes Element als übergeordnetes Element für eine Tabelle mit untergeordneten Elementen fungieren kann.
Bevor man versucht, ein Baummodell zu implementieren, sollte man sich überlegen, ob die Daten aus einer externen Quelle stammen oder ob sie im Modell selbst gepflegt werden sollen. In diesem Beispiel werden wir eine interne Struktur zur Datenhaltung implementieren und nicht erörtern, wie man Daten aus einer externen Quelle verpackt.
Entwurf und Konzepte
Die Datenstruktur, die wir verwenden, um die Struktur der Daten darzustellen, hat die Form eines Baums, der aus TreeItem
Objekten besteht. Jedes TreeItem
stellt ein Element in einer Baumansicht dar und enthält mehrere Spalten mit Daten.
![]() | Einfache Baummodellstruktur Die Daten werden intern im Modell mit Jedes Da jedes Element in einer Baumansicht in der Regel mehrere Spalten mit Daten enthält (in diesem Beispiel einen Titel und eine Zusammenfassung), ist es naheliegend, diese Informationen in jedem Element zu speichern. Der Einfachheit halber werden wir eine Liste von QVariant Objekten verwenden, um die Daten für jede Spalte im Element zu speichern. |
Die Verwendung einer zeigerbasierten Baumstruktur bedeutet, dass wir bei der Übergabe eines Modellindex an eine Ansicht die Adresse des entsprechenden Elements im Index aufzeichnen können (siehe QAbstractItemModel::createIndex()) und sie später mit QModelIndex::internalPointer() abrufen können. Dies erleichtert das Schreiben des Modells und gewährleistet, dass alle Modellindizes, die sich auf dasselbe Element beziehen, denselben internen Datenzeiger haben.
Mit der entsprechenden Datenstruktur können wir ein Baummodell mit einem Minimum an zusätzlichem Code erstellen, um Modellindizes und Daten an andere Komponenten zu liefern.
TreeItem Klassendefinition
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 bietet keine Signale und Slots. Sie wird verwendet, um eine Liste von QVarianten zu speichern, die Spaltendaten und Informationen über ihre Position in der Baumstruktur enthält. Die Funktionen bieten die folgenden Eigenschaften:
- Die Funktion
appendChildItem()
wird verwendet, um Daten hinzuzufügen, wenn das Modell zum ersten Mal konstruiert wird, und wird während des normalen Gebrauchs nicht verwendet. - Die Funktionen
child()
undchildCount()
ermöglichen es dem Modell, Informationen über alle untergeordneten Elemente zu erhalten. - Informationen über die Anzahl der Spalten, die mit dem Element verbunden sind, werden von
columnCount()
bereitgestellt, und die Daten in jeder Spalte können mit der Funktion data() abgerufen werden. - Die Funktionen
row()
undparent()
werden verwendet, um die Zeilennummer des Elements und das übergeordnete Element zu ermitteln.
Die Daten des übergeordneten Elements und der Spalten werden in den privaten Mitgliedsvariablen parentItem
und itemData
gespeichert. Die Variable childItems
enthält eine Liste von Zeigern auf die eigenen untergeordneten Elemente des Elements.
TreeItem Klasse Implementierung
Der Konstruktor wird nur verwendet, um das übergeordnete Element und die mit jeder Spalte verbundenen Daten zu speichern.
TreeItem::TreeItem(QVariantList data, TreeItem *parent) : m_itemData(std::move(data)), m_parentItem(parent) {}
Ein Zeiger auf jeden untergeordneten Eintrag, der zu diesem Eintrag gehört, wird in der privaten Membervariable 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 alle untergeordneten Elemente konstruiert werden, wenn das Modell zum ersten Mal mit Daten gefüllt wird, ist die Funktion zum Hinzufügen untergeordneter Elemente einfach zu handhaben:
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child) { m_childItems.push_back(std::move(child)); }
Jedes Element ist in der Lage, jedes seiner untergeordneten Elemente zurückzugeben, wenn es eine geeignete Zeilennummer erhält. Im obigen Diagramm entspricht beispielsweise das mit dem Buchstaben "A" gekennzeichnete Element dem untergeordneten Element des Stammelements mit row = 0
, das Element "B" ist ein untergeordnetes Element des Elements "A" mit row = 1
und das Element "C" ist ein untergeordnetes Element des Stammelements mit row = 1
.
Die Funktion child()
gibt das untergeordnete Element zurück, das der angegebenen Zeilennummer in der Liste der untergeordneten Elemente des Elements entspricht:
TreeItem *TreeItem::child(int row) { return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr; }
Die Anzahl der enthaltenen untergeordneten Elemente kann mit childCount()
ermittelt werden:
int TreeItem::childCount() const { return int(m_childItems.size()); }
TreeModel
verwendet diese Funktion, um die Anzahl der Zeilen zu ermitteln, die für einen bestimmten übergeordneten Artikel vorhanden sind.
Die Funktion row()
meldet die Position des Elements in der Liste der Elemente des übergeordneten Elements:
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, obwohl dem Stammelement (ohne übergeordnetes Element) automatisch eine Zeilennummer von 0 zugewiesen wird, diese Information nie vom Modell verwendet wird.
Die Anzahl der Datenspalten im Element wird trivialerweise von der Funktion columnCount()
zurückgegeben.
int TreeItem::columnCount() const { return int(m_itemData.count()); }
Die Spaltendaten werden von der Funktion data()
zurückgegeben. Wir verwenden die Komfortfunktion QList::value(), die die Grenzen prüft und eine standardmäßig konstruierte QVariant zurückgibt, falls sie verletzt werden:
QVariant TreeItem::data(int column) const { return m_itemData.value(column); }
Das Elternteil des Elements wird mit parent()
gefunden:
TreeItem *TreeItem::parentItem() { return m_parentItem; }
Da das Stammelement im Modell kein übergeordnetes Element hat, gibt diese Funktion in diesem Fall Null zurück. Wir müssen sicherstellen, dass das Modell diesen Fall korrekt behandelt, wenn wir die Funktion TreeModel::parent()
implementieren.
TreeModel Klassendefinition
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. Nur die Form des Konstruktors und die Funktion setupModelData()
sind spezifisch für dieses Modell. Darüber hinaus bieten wir einen Destruktor, um das Modell zu bereinigen, wenn es zerstört wird.
TreeModel Klasse Implementierung
Der Einfachheit halber erlaubt das Modell nicht, dass seine Daten bearbeitet werden. Daher nimmt der Konstruktor ein Argument entgegen, das die Daten enthält, die das Modell mit den Ansichten und Delegierten teilen wird:
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 Wurzelelement für das Modell zu erstellen. Dieses Element enthält der Einfachheit halber nur vertikale Kopfdaten. Wir verwenden es auch, um auf die interne Datenstruktur zu verweisen, die die Modelldaten enthält, und es wird verwendet, um ein imaginäres übergeordnetes Element der Top-Level-Elemente im Modell darzustellen. Das Wurzelelement wird mit einer std::unique_ptr verwaltet, um sicherzustellen, dass der gesamte Baum der Elemente 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 untersuchen.
Der Destruktor sorgt dafür, dass das Wurzelelement und alle seine Nachkommen gelöscht werden, wenn das Modell zerstört wird. Dies geschieht automatisch, da das Wurzelelement in einer unique_ptr gespeichert ist.
TreeModel::~TreeModel() = default;
Da wir dem Modell keine Daten hinzufügen können, nachdem es konstruiert und eingerichtet wurde, vereinfacht dies die Verwaltung des internen Baums der Elemente.
Modelle müssen eine index()
Funktion implementieren, um Indizes für Views und Delegates bereitzustellen, die beim Zugriff auf Daten verwendet werden. Indizes werden für andere Komponenten erstellt, wenn sie durch ihre Zeilen- und Spaltennummern und ihren übergeordneten Modellindex referenziert werden. Wenn ein ungültiger Modellindex als übergeordnetes Element angegeben wird, obliegt es dem Modell, einen Index zurückzugeben, der einem Element der obersten Ebene des Modells entspricht.
Wenn wir einen Modellindex erhalten, prüfen wir zunächst, ob er gültig ist. Ist dies nicht der Fall, nehmen wir an, dass auf ein Element der obersten Ebene verwiesen wird; andernfalls erhalten wir den Datenzeiger aus dem Modellindex mit der Funktion internalPointer() und verwenden ihn, um auf ein TreeItem
Objekt zu verweisen. Beachten Sie, dass alle Modellindizes, die wir konstruieren, einen Zeiger auf ein vorhandenes TreeItem
enthalten, so dass wir garantieren können, dass alle gültigen Modellindizes, die wir erhalten, einen gültigen Datenzeiger enthalten.
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 die Zeilen- und Spaltenargumente für diese Funktion auf ein untergeordnetes Element des entsprechenden übergeordneten Elements verweisen, erhalten wir das Element mit der Funktion TreeItem::child()
. Die Funktion createIndex() wird verwendet, um einen Modellindex zu erstellen, der zurückgegeben wird. 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 zu erhalten.
Die Art und Weise, wie die Objekte TreeItem
definiert sind, macht das Schreiben der Funktion parent()
einfach:
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 nur sicherstellen, dass wir niemals einen Modellindex zurückgeben, der dem Stammelement entspricht. Um mit der Art und Weise, wie die Funktion index()
implementiert ist, konsistent zu sein, geben wir einen ungültigen Modellindex für das übergeordnete Element aller Top-Level-Elemente im Modell 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 kann leicht mit der Funktion TreeItem::row()
ermittelt werden, aber wir folgen der Konvention, 0 als Spaltennummer des übergeordneten Elements anzugeben. Der Modellindex wird mit createIndex() auf die gleiche Weise erstellt wie in der Funktion index()
.
Die Funktion rowCount()
gibt einfach die Anzahl der untergeordneten Elemente für TreeItem
zurück, die einem gegebenen Modellindex entsprechen, oder die Anzahl der Elemente der obersten Ebene, wenn 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 Element-eigene Funktion columnCount()
aufrufen, um festzustellen, wie viele Spalten für einen bestimmten Modellindex vorhanden sind. Wie bei der Funktion rowCount()
wird, wenn ein ungültiger Modellindex angegeben wird, die Anzahl der zurückgegebenen Spalten anhand des Stammelements bestimmt:
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 bezogen. 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 DisplayRole unterstützen und bei ungültigen Modellindizes auch ungültige QVariant Objekte zurückgeben.
Wir verwenden die Funktion flags()
, um sicherzustellen, dass die Ansichten wissen, 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 durch Angabe im Konstruktor oder durch Festcodierung in der Funktion headerData()
.
Einrichten der Daten im Modell
Wir verwenden die Funktion setupModelData()
, um die Anfangsdaten im Modell einzurichten. Diese Funktion analysiert eine Textdatei, extrahiert die im Modell zu verwendenden Textzeichenfolgen und erstellt Elementobjekte, die sowohl die Daten als auch die Gesamtstruktur des Modells aufzeichnen. Natürlich arbeitet diese Funktion auf eine Weise, die sehr spezifisch für dieses Modell ist. Wir beschreiben im Folgenden ihr Verhalten und verweisen den Leser für weitere Informationen auf den Beispielcode selbst.
Wir beginnen mit einer Textdatei in folgendem 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 connections
Wir verarbeiten die Textdatei mit den folgenden zwei Regeln:
- Für jedes Zeichenkettenpaar in jeder Zeile wird ein Element (oder Knoten) in einer Baumstruktur erstellt und jede Zeichenkette in eine Datenspalte des Elements eingefügt.
- Wenn die erste Zeichenkette in einer Zeile in Bezug auf die erste Zeichenkette in der vorherigen Zeile eingerückt ist, wird das Element zu einem Kind des zuvor erstellten Elements.
Um sicherzustellen, dass das Modell korrekt funktioniert, müssen nur Instanzen von TreeItem
mit den richtigen Daten und dem übergeordneten Element erstellt werden.
Testen des Modells
Die korrekte Implementierung eines Objektmodells kann eine Herausforderung sein. Die Klasse QAbstractItemModelTester aus dem Qt Test Modul prüft die Konsistenz des Modells, z. B. die Erstellung des Modellindex und die Eltern-Kind-Beziehungen.
Sie können Ihr Modell testen, indem Sie einfach eine Modellinstanz an den Klassenkonstruktor übergeben, z. B. als Teil eines Qt-Unit-Tests:
class TestSimpleTreeModel : public QObject { Q_OBJECTprivate slots: void testTreeModel(); };void TestSimpleTreeModel::testTreeModel() { constexpr auto fileName = ":/default.txt"_L1; QFile file(Dateiname); 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 der ausführbaren Datei 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" PREFIX "/" FILES ${simpletreemodel_resource_files} ) add_test(NAME simpletreemodel_tester COMMAND simpletreemodel_tester)
© 2025 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.