可编辑树模型示例
本示例演示了如何实现一个简单的基于项的树模型,该模型可与模型/视图框架中的其他类配合使用。

该模型支持可编辑项目、自定义标题,以及插入和删除行与列的功能。借助这些功能,还可以插入新的子项目,配套的示例代码中展示了这一功能。
概述
如《模型子类化参考》所述,模型必须为一组标准模型函数提供实现:flags()、data()、headerData()、columnCount() 以及rowCount()。此外,像本例这样的分层模型还需要提供index() 和parent() 的实现。
可编辑模型需要提供setData() 和setHeaderData() 的实现,并且其flags() 函数必须返回一组合适的标志组合。
由于本示例允许更改模型的维度,因此我们还必须实现insertRows()、insertColumns()、removeRows() 和removeColumns() 方法。
设计
与“简单树模型”示例类似,该模型只是作为TreeItem 类的一组实例的封装器。每个TreeItem 都设计用于存储树视图中一行项的数据,因此它包含一个值列表,这些值对应于各列中显示的数据。
由于QTreeView 为模型提供了一种行导向的视图,因此对于那些将通过模型向此类视图提供数据的数据结构,选择行导向的设计是顺理成章的。尽管这会降低树模型的灵活性,并且在与更复杂的视图配合使用时可能不太实用,但它使设计更简单,实现也更容易。
![]() | 内部项之间的关系 在为自定义模型设计数据结构时,通过TreeItem::parent()之类的函数暴露每个项的父项非常有用,因为这将使编写模型自身的parent() 函数变得更加容易。 同样,在实现模型的 `index()` 函数时,类似`TreeItem::child()`的函数也会很有帮助。因此,每个 ` 该图展示了 在所示示例中,可以通过调用根节点的 child() 函数获得两个顶级项A和B;虽然图中仅展示了项 A,但这两个项的 parent() 函数均会返回根节点。 |
每个TreeItem 都会在其私有成员itemData (一个QVariant 对象的列表)中,为其所代表的行中的每一列存储数据。由于视图中的每一列与列表中的每一项之间存在一对一映射关系,因此我们提供了一个简单的数据()函数来读取itemData 列表中的条目,以及一个setData()函数来允许对其进行修改。 与该项中的其他函数一样,这简化了模型中data()和setData()函数的实现。
我们将一个项放置在项树的根节点处。 该根项对应于空模型索引QModelIndex(),该索引在处理模型索引时用于表示顶级项的父项。尽管根项在任何标准视图中都没有可见的表示,但我们使用其内部的QVariant 对象列表来存储一个字符串列表,该列表将被传递给视图,用作水平标题。
![]() | 通过模型访问数据 在图中所示的情况下,可以通过标准的模型/视图 API 获取由a表示的信息: 由于每个`items`对象都包含给定行中各列的数据,因此可能存在多个模型索引映射到同一个 要获取与b 位于同一行中的其他模型索引的信息,也将访问相同的底层 ` |
在模型类TreeModel 中,我们通过在index() 和parent()方法的实现中,使用QAbstractItemModel::createIndex()创建对应模型索引时,为每个TreeItem 对象传递一个指针,从而将其与模型索引建立关联。 我们可以通过在相关模型索引上调用internalPointer()函数来检索以这种方式存储的指针——我们创建了自己的getItem()函数来完成这项工作,并在data() 和parent()的实现中调用它。
当我们能够控制项的创建和销毁方式时,存储项的指针非常方便,因为我们可以假设从internalPointer() 获取的地址是一个有效的指针。但是,有些模型需要处理从系统中其他组件获取的项,而在许多情况下,无法完全控制项的创建或销毁方式。 在这种情况下,纯指针式方法需要辅以保护措施,以确保模型不会尝试访问已被删除的项。
| 在底层数据结构中存储信息 若干数据以QVariant 对象的形式存储在每个 该图展示了前两张图中由标签a、b和c表示的信息片段,是如何存储在底层数据结构中的项A、B和C中的。 请注意,模型中同一行中的信息片段均来自同一个项。列表中的每个元素都对应于模型中给定行中各列所展示的一条信息。 | ![]() |
由于TreeModel 的实现是为配合QTreeView 而设计的,因此我们对它使用TreeItem 实例的方式添加了一项限制:每个项必须暴露相同数量的数据列。 这使得模型的浏览保持一致——我们可以利用根项来确定任意给定行中的列数,同时仅要求我们创建的项包含足以覆盖总列数的数据。因此,插入和删除列是耗时的操作,因为我们需要遍历整个树来修改每个项。
另一种方法是设计TreeModel 类,使其在修改数据项时自动截断或扩展各个TreeItem 实例中的数据列表。然而,这种“懒加载”式的调整大小方法仅允许在每行末尾插入或删除列,而无法在每行的任意位置插入或删除列。
![]() | 使用模型索引关联项目 与“简单树模型”示例类似, 在该图中,我们展示了模型的parent()实现如何利用前一图中所示的项目,获取与调用方提供的某项的父项相对应的模型索引。 通过调用QModelIndex::internalPointer()函数,可以从相应的模型索引中获取指向项目C的指针。 该指针在索引创建时已被内部存储其中。由于子项包含指向其父项的指针,我们使用其parent()函数来获取指向项B 的指针。通过调用QAbstractItemModel::createIndex() 函数并传入指向项B的指针作为内部指针,从而创建父项的模型索引。 |
TreeItem 类定义
TreeItem 类提供了一系列简单的项,这些项包含多项数据,包括关于其父项和子项的信息:
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;
};我们设计了与QAbstractItemModel 提供的API相似的接口,为每个项目提供了用于返回信息列数、读写数据以及插入和删除列的函数。不过,我们通过提供处理“子项”而非“行”的函数,将项目之间的关系明确化。
每个项包含一个指向子项的指针列表、一个指向其父项的指针,以及一个QVariant 对象列表,这些对象对应于模型中给定行内各列所包含的信息。
TreeItem 类的实现
每个 `TreeItem ` 都是通过一组数据和一个可选的父项来构建的:
TreeItem::TreeItem(QVariantList data, TreeItem *parent)
: itemData(std::move(data)), m_parentItem(parent)
{}初始时,每个项都没有子项。子项将通过下文所述的insertChildren() 函数添加到该项的内部childItems 成员中。
子项存储在 std::unique_ptr 中,以确保当该项被删除时,添加到该项的每个子项也会被删除。
由于每个项都存储着指向其父项的指针,因此parent() 函数非常简单:
TreeItem *TreeItem::parent()
{
return m_parentItem;
}有三个函数提供有关项的子项的信息。child() 函数从内部子项列表中返回特定的子项:
TreeItem *TreeItem::child(int number)
{
return (number >= 0 && number < childCount())
? m_childItems.at(number).get() : nullptr;
}childCount() 函数返回子节点的总数:
int TreeItem::childCount() const
{
return int(m_childItems.size());
}row() 函数用于确定子节点在其父节点的子节点列表中的索引。它通过直接访问父节点的childItems 成员来获取此信息:
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;
}根节点没有父节点;对于该节点,为了与其他节点保持一致,我们返回零。
columnCount() 函数仅返回内部itemData 列表中QVariant 对象的元素个数:
int TreeItem::columnCount() const
{
return int(itemData.count());
}数据是通过data() 函数获取的,该函数会访问itemData 列表中的相应元素:
QVariant TreeItem::data(int column) const
{
return itemData.value(column);
}数据通过setData() 函数进行设置,该函数仅将值存储在itemData 列表中,且这些值对应的必须是有效的列表索引,即与模型中的列值相对应:
bool TreeItem::setData(int column, const QVariant &value)
{
if (column < 0 || column >= itemData.size())
return false;
itemData[column] = value;
return true;
}为了便于模型的实现,我们返回 true 以表示数据已成功设置。
可编辑的模型通常需要支持调整大小,以便插入和删除行与列。在模型中某个给定索引下方插入行,将导致在相应项中插入新的子项,此操作由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;
}这确保了新项会以所需的列数创建,并插入到childItems 内部列表中的有效位置。项的删除则通过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;
}如上所述,插入和删除列的函数与插入和删除子项的函数使用方式不同,因为前者需要对树中的每个项进行调用。我们通过对该项的每个子项递归调用此函数来实现:
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;
}TreeModel 类定义
TreeModel 类提供了QAbstractItemModel 类的实现,并为可编辑和可调整大小的模型提供了必要的接口。
class TreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
Q_DISABLE_COPY_MOVE(TreeModel)
TreeModel(const QStringList &headers, const QString &data,
QObject *parent = nullptr);
~TreeModel() override;构造函数和析构函数是该模型特有的。
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;只读树模型只需提供上述函数即可。以下公共函数为编辑和调整大小提供了支持:
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;
};为简化本示例,模型通过setupModelData()函数将公开的数据组织成一种数据结构。许多实际应用中的模型根本不会处理原始数据,而是直接基于现有的数据结构或库 API 进行操作。
TreeModel 类的实现
构造函数会创建一个根项,并使用提供的标头数据对其进行初始化:
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'));
}我们调用内部函数setupModelData(),将提供的文本数据转换为模型可用的数据结构。其他模型可能使用现成的数据结构进行初始化,或者使用来自某个库的 API,该库会自行维护数据。
TreeModel::~TreeModel() = default;析构函数只需删除根节点,这将导致所有子节点被递归删除。由于根节点存储在 unique_ptr 中,因此默认析构函数会自动完成此操作。
由于模型与其他模型/视图组件的接口基于模型索引,且内部数据结构基于项,因此模型实现的许多函数都需要能够将给定的模型索引转换为相应的项。为了方便和保持一致性,我们定义了getItem() 函数来执行这项重复性任务:
TreeItem *TreeModel::getItem(const QModelIndex &index) const
{
if (index.isValid()) {
if (auto *item = static_cast<TreeItem*>(index.internalPointer()))
return item;
}
return rootItem.get();
}传递给该函数的每个模型索引都应对应内存中一个有效的项。如果索引无效,或者其内部指针未指向有效的项,则返回根项。
该模型的 `rowCount() ` 实现很简单:它首先使用 `getItem() ` 函数获取相关项;然后返回该项包含的子项数量:
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;
}相比之下,columnCount() 的实现无需查找特定项,因为所有项都被定义为具有相同数量的关联列。
int TreeModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return rootItem->columnCount();
}因此,可以直接从根项目中获取列数。
为了使项目能够被编辑和选中,需要实现flags() 函数,使其返回一个包含Qt::ItemIsEditable 和Qt::ItemIsSelectable 标志以及Qt::ItemIsEnabled 标志的标志组合:
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}模型必须能够生成模型索引,以便其他组件请求其结构的相关数据和信息。此任务由index() 函数完成,该函数用于获取与给定父项的子项对应的模型索引:
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0)
return {};在此模型中,我们仅在父项索引无效(即对应根项)或其列号为零时,才返回子项的模型索引。
我们使用自定义的 `getItem()` 函数来获取一个与给定模型索引对应的 `TreeItem ` 实例,并请求其对应于指定行号的子项。
TreeItem *parentItem = getItem(parent);
if (!parentItem)
return {};
if (auto *childItem = parentItem->child(row))
return createIndex(row, column, childItem);
return {};
}由于每个项都包含整行数据的信息,因此我们通过调用createIndex()并传入行号、列号以及该项的指针,来创建一个用于唯一标识该项的模型索引。 在data()函数中,我们将使用项指针和列号来访问与模型索引关联的数据;在此模型中,无需行号即可识别数据。
parent() 函数通过为项目提供模型索引来确定其父项:首先根据给定的模型索引查找对应的项目,然后使用该项目的parent()函数获取其父项,最后创建一个模型索引来表示该父项。(参见上图)。
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{};
}对于没有父节点的项目(包括根项目),系统会返回一个空模型索引。否则,会像index()函数那样创建并返回一个模型索引,其中行号为适当数值,但列号设为零,以保持与index()实现中采用的方案一致。
测试模型
QAbstractItemModelTester 正确实现项目模型可能颇具挑战性。来自 Qt Test 模块中的xml-ph-0000@deepl.internal类会检查模型的一致性,例如模型索引的创建以及父子关系。
您只需将模型实例传递给该类的构造函数,即可测试您的模型,例如作为 Qt Test 单元测试的一部分:
classTestEditableTreeModel :publicQObject
{
Q_OBJECT
private slots:
voidtestTreeModel();
};
voidTestEditableTreeModel::testTreeModel()
{
constexprautofileName= ":/default.txt"_L1;
QFile file(fileName);
QVERIFY2(file.open(QIODevice::ReadOnly|QIODevice::Text),
qPrintable(fileName + " cannot be opened: "_L1 + file.errorString()));
constQStringList headers{"column1"_L1, "column2"_L1};
TreeModel model(headers,QString::fromUtf8(file.readAll()));
QAbstractItemModelTester tester(&model);
}
QTEST_APPLESS_MAIN(TestEditableTreeModel)
#include "test.moc"要创建一个可通过ctest 可执行文件运行的测试,需使用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.



