简单树模型示例
“简单树模型”示例演示了如何将分层模型与 Qt 的标准视图类结合使用。

Qt 的模型/视图架构为视图提供了一种标准方式,用于操作数据源中的信息,通过使用数据的抽象模型来简化和标准化对数据的访问方式。简单模型将数据表示为项目表,并允许视图通过基于索引的系统访问这些数据。 更普遍地说,模型可以通过让每个项目作为子项目表的父项,将数据表示为树结构的形式。
在尝试实现树模型之前,值得考虑的是数据是由外部来源提供的,还是将在模型内部维护。在本示例中,我们将实现一个内部结构来存储数据,而不是讨论如何封装来自外部来源的数据。
设计与概念
我们用来表示数据结构的数据结构,采用由TreeItem 对象构成的树形结构。每个TreeItem 都代表树视图中的一个项目,并包含多列数据。
| 简易树模型结构 数据在模型内部通过 每个 由于树视图中的每个项目通常包含多列数据(本例中为标题和摘要),将这些信息存储在每个项目中是自然而然的选择。为简化起见,我们将使用一个QVariant 对象列表来存储项目中各列的数据。 |
使用基于指针的树结构意味着,当向视图传递模型索引时,我们可以记录索引中对应项的地址(参见QAbstractItemModel::createIndex()),并稍后通过QModelIndex::internalPointer() 将其检索出来。这简化了模型的编写,并确保所有引用同一项的模型索引都拥有相同的内部数据指针。
有了合适的数据结构,我们只需编写极少的额外代码,即可创建一个树形模型,从而向其他组件提供模型索引和数据。
TreeItem 类定义
TreeItem 类的定义如下:
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;
};该类是一个基本的 C++ 类。它既不继承自QObject ,也不提供信号和插槽。它用于存储一个包含列数据的 QVariant 列表,以及该列表在树结构中的位置信息。该类提供的函数具有以下功能:
appendChildItem()用于在模型首次构建时添加数据,在正常使用过程中不被调用。child()和childCount()函数允许模型获取任何子项的相关信息。columnCount()函数提供与该项目关联的列数信息,而各列中的数据可通过 data() 函数获取。row()和parent()函数用于获取该项的行号和父项。
父项和列数据分别存储在parentItem 和itemData 这两个私有成员变量中。childItems 变量包含指向该项自身子项的指针列表。
TreeItem 类的实现
构造函数仅用于记录项的父项以及与各列相关的数据。
TreeItem::TreeItem(QVariantList data, TreeItem *parent)
: m_itemData(std::move(data)), m_parentItem(parent)
{}属于该项的每个子项的指针将作为 std::unique_ptr 存储在私有成员变量 `childItems ` 中。当调用该类的析构函数时,子项将被自动删除,以确保其内存被重新利用:
由于每个子项都是在模型初始填充数据时构造的,因此添加子项的函数非常简单:
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child)
{
m_childItems.push_back(std::move(child));
}给定一个合适的行号后,每个项都能返回其任意一个子项。 例如,在上图中,标记为字母“A”的项目对应于根项目的子项目,其行号为row = 0 ;标记为“B”的项目是“A”项目的子项目,其行号为row = 1 ;而标记为“C”的项目是根项目的子项目,其行号为row = 1 。
child() 函数返回在项的子项列表中与指定行号相对应的子项:
TreeItem *TreeItem::child(int row)
{
return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr;
}可通过childCount() 获取子项的数量:
int TreeItem::childCount() const
{
return int(m_childItems.size());
}TreeModel 通过此函数来确定给定父项对应的行数。
row() 函数会报告该项目在其父项目列表中的位置:
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;
}请注意,尽管根项(无父项)会被自动分配行号 0,但模型绝不会使用此信息。
columnCount() 函数会直接返回该项目中数据的列数。
int TreeItem::columnCount() const
{
return int(m_itemData.count());
}data() 函数返回数据的列数。我们使用QList::value()这个便捷函数,它会检查边界,并在边界被违反时返回一个默认构造的QVariant :
QVariant TreeItem::data(int column) const
{
return m_itemData.value(column);
}通过parent() 查找该项的父项:
TreeItem *TreeItem::parentItem()
{
return m_parentItem;
}请注意,由于模型中的根项不会有父项,因此在此情况下该函数将返回零。在实现TreeModel::parent() 函数时,我们需要确保模型能正确处理这种情况。
TreeModel 类定义
TreeModel 类的定义如下:
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;
};该类与QAbstractItemModel 的大多数其他子类类似,均提供只读模型。只有构造函数的形式和setupModelData() 函数是该模型特有的。此外,我们还提供了一个析构函数,用于在模型销毁时进行资源清理。
TreeModel 类的实现
为简化起见,该模型不允许对其数据进行编辑。因此,构造函数接受一个参数,其中包含模型将与视图和委托共享的数据:
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());
}由构造函数负责为模型创建一个根项。出于方便起见,该项仅包含垂直标题数据。 我们还利用它来引用包含模型数据的内部数据结构,并将其用作模型中顶级项的虚拟父项。根项通过 std::unique_ptr 进行管理,以确保在删除模型时,整个项树会被一并删除。
模型的内部数据结构由setupModelData() 函数填充项。我们将在本文档结尾处单独探讨该函数。
析构函数确保在模型销毁时,根项及其所有子项均被删除。由于根项存储在 unique_ptr 中,因此该操作会自动完成。
TreeModel::~TreeModel() = default;由于模型在构建并初始化完成后无法再向其中添加数据,这简化了内部项树的管理方式。
模型必须实现index() 函数,以提供视图和委托在访问数据时所需的索引。 当其他组件通过其行号、列号及其父模型索引被引用时,系统会为其创建索引。如果指定的父模型索引无效,则由模型自行返回一个对应于该模型中顶级项的索引。
当提供模型索引时,我们会首先检查其是否有效。若无效,则假设所引用的是顶级项;否则,我们将通过模型的internalPointer()函数从该索引获取数据指针,并用其引用TreeItem 对象。 请注意,我们构建的所有模型索引都将包含指向现有TreeItem 的指针,因此我们可以保证,收到的任何有效模型索引都将包含一个有效的数据指针。
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 {};
}由于该函数的行和列参数分别指向相应父项的子项,因此我们使用TreeItem::child() 函数获取该项。createIndex()函数用于创建一个待返回的模型索引。我们指定行号和列号,以及指向该项本身的指针。该模型索引可在后续用于获取该项的数据。
TreeItem 对象的定义方式使得编写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{};
}我们只需确保绝不返回与根项对应的模型索引。为了与index() 函数的实现方式保持一致,对于模型中任何顶级项的父项,我们返回一个无效的模型索引。
在创建要返回的模型索引时,我们必须指定父项在其自身父项中的行号和列号。我们可以利用TreeItem::row() 函数轻松获取行号,但根据约定,父项的列号应设为 0。模型索引的创建方式与index() 函数中的createIndex() 相同。
rowCount() 函数仅返回与给定模型索引对应的TreeItem 中的子项数量;若指定了无效索引,则返回顶级项的数量:
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();
}由于每个项都管理着自己的列数据,因此columnCount() 函数必须调用该项自身的columnCount() 函数,以确定给定模型索引下存在多少列。与rowCount() 函数一样,如果指定了无效的模型索引,则返回的列数将根据根项确定:
int TreeModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return static_cast<TreeItem*>(parent.internalPointer())->columnCount();
return rootItem->columnCount();
}数据是通过data() 从模型中获取的。由于项目管理着自己的列,我们需要使用列号通过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());
}请注意,在此实现中我们仅支持 `DisplayRole `,对于无效的模型索引,我们会返回无效的 `QVariant ` 对象。
我们使用flags() 函数来确保视图能够识别该模型为只读:
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
return index.isValid()
? QAbstractItemModel::flags(index) : Qt::ItemFlags(Qt::NoItemFlags);
}headerData() 函数返回了我们事先存储在根项中的数据:
QVariant TreeModel::headerData(int section, Qt::Orientation orientation,
int role) const
{
return orientation == Qt::Horizontal && role == Qt::DisplayRole
? rootItem->data(section) : QVariant{};
}这些信息也可以通过其他方式提供:要么在构造函数中指定,要么硬编码到 `headerData() ` 函数中。
在模型中设置数据
我们使用setupModelData() 函数来设置模型中的初始数据。该函数会解析一个文本文件,从中提取用于模型的文本字符串,并创建项目对象,这些对象既记录数据,也记录整体模型结构。当然,该函数的工作方式非常特定于此模型。 下面将对其行为进行说明,更多详细信息请参阅示例代码本身。
我们从一个具有以下格式的文本文件开始:
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我们按照以下两条规则对该文本文件进行处理:
- 对于每行中的每对字符串,在树结构中创建一个项(或节点),并将每个字符串放置在该项的数据列中。
- 当某行中的第一个字符串相对于上一行的第一个字符串缩进时,将该项设为之前创建的项的子项。
为了确保模型能够正确运行,只需使用正确的数据和父项来创建TreeItem 的实例即可。
测试模型
QAbstractItemModelTester 正确实现项目模型可能颇具挑战。来自 Qt Test 模块中的xml-ph-0000@deepl.internal类,用于检查模型的一致性,例如模型索引的创建以及父子关系。
您只需将模型实例传递给该类的构造函数,即可测试您的模型,例如作为 Qt Test 单元测试的一部分:
classTestSimpleTreeModel :publicQObject
{
Q_OBJECT
private slots:
voidtestTreeModel();
};
voidTestSimpleTreeModel::testTreeModel()
{
constexprautofileName= ":/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"要创建一个可通过ctest 可执行文件运行的测试,需使用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.