可编辑树模型示例
本示例展示了如何实现一个简单的基于项目的树形模型,该模型可与模型/视图框架中的其他类一起使用。
该模型支持可编辑项、自定义标题以及插入和移除行和列的功能。有了这些功能,还可以插入新的子项,这在支持的示例代码中有所展示。
概述
正如《模型子类化参考》中所述,模型必须提供标准模型函数集的实现:flags(),data(),headerData(),columnCount() 和rowCount() 。此外,分层模型(如本模型)需要提供index() 和parent() 的实现。
可编辑模型需要提供setData() 和setHeaderData() 的实现,并且必须从flags() 函数返回一个合适的标志组合。
由于此示例允许更改模型的尺寸,我们还必须实现insertRows(),insertColumns(),removeRows() 和removeColumns().
设计
与简单树模型示例一样,该模型只是作为TreeItem
类实例集合的包装。每个TreeItem
都用于保存树形视图中一行项目的数据,因此它包含与每列中显示的数据相对应的值列表。
由于QTreeView 提供了一个面向行的模型视图,因此很自然地选择了面向行的数据结构设计,通过模型向这种视图提供数据。虽然这使得树状模型的灵活性降低,在使用更复杂的视图时也可能不那么有用,但它使设计变得不那么复杂,也更容易实现。
![]() | 内部项之间的关系 在设计与自定义模型一起使用的数据结构时,通过TreeItem::parent()这样的函数公开每个项的父项是非常有用的,因为这样可以让编写模型自己的parent() 函数变得更容易。同样,像TreeItem::child()这样的函数也有助于实现模型的index() 函数。因此,每个 图中显示了 在所示示例中,两个顶层项A和B 可以通过调用根项的 child() 函数从根项获取,而每个项都会从其 parent() 函数中返回根节点,但只显示了项A。 |
每个TreeItem
都会在其itemData
私有成员(一个QVariant 对象列表)中存储所代表行中每一列的数据。由于视图中的每一列与列表中的每个条目之间都存在一对一的映射关系,因此我们提供了一个简单的data()函数来读取itemData
列表中的条目,并提供了一个setData()函数来修改这些条目。与项目中的其他函数一样,这简化了模型的data() 和setData() 函数的实现。
我们在项目树的根部放置一个项目。这个根项对应于空模型索引QModelIndex() ,在处理模型索引时,它被用来表示顶层项的父项。虽然根项目在任何标准视图中都没有可见的表示,但我们使用其内部的QVariant 对象列表来存储字符串列表,这些字符串将作为水平标题传递给视图。
![]() | 通过模型访问数据 在图中所示的情况下,可以使用标准的模型/视图 API 获取a所代表的信息: QVariant a = model->index(0, 0, QModelIndex()).data(); 由于每个项都包含给定行中每一列的数据块,因此可以有很多模型索引映射到同一个 QVariant b = model->index(1, 0, QModelIndex()).data(); 访问相同的底层 |
在模型类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的指针。该指针在创建索引时存储在索引内部。由于子代包含指向其父代的指针,我们使用其父代()函数获取指向项目B 的指针。父代模型索引是使用QAbstractItemModel::createIndex() 函数创建的,并将指向项目B的指针作为内部指针传入。 |
树项类定义
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 提供的类似,为每个项提供了返回信息列数、读写数据以及插入和删除列的函数。不过,通过提供处理 "子项 "而非 "行 "的函数,我们明确了项与项之间的关系。
每个项都包含一个指向子项的指针列表、一个指向其父项的指针,以及一个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; }
根项目没有父项目;为了与其他项目保持一致,我们将该项目返回 0。
columnCount()
函数只是返回QVariant 对象的内部itemData
列表中的元素个数:
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
类提供了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(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()
函数为项的父项提供模型索引,方法是为给定的模型索引查找相应的项,使用父项()函数获取其父项,然后创建一个模型索引来表示父项。(见上图)。
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模块中的类检查模型的一致性,如模型索引的创建和父子关系。
您只需向类构造函数传递一个模型实例即可测试您的模型,例如作为 Qt 单元测试的一部分:
classTestEditableTreeModel :publicQObject {Q_OBJECTprivate slots:voidtestTreeModel(); };voidTestEditableTreeModel::testTreeModel() { constexprautofileName= ":/default.txt"_L1; QFilefile(fileName); QVERIFY2(file.open(QIODevice: :只读 QIODevice::Text)、 qPrintable(fileName + " cannot be opened: "_L1 + file.errorString())); 常量QStringListheaders{"column1"_L1, "column2"_L1}; TreeModel model(headers、 QStringTreeModel model(headers,::fromUtf8(file.readAll())); QAbstractItemModelTestertester(&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)
© 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.