注意

本节包含自动从C++转换为Python的代码片段,可能包含错误。

简单树型模型示例#

简单树型模型示例展示了如何使用Qt标准视图类来使用分层模型。

简单树型模型示例展示了如何创建一个基本的、只读的分层模型并将其与Qt的标准视图类一起使用。对于简单非分层列表和表格模型的描述,请参阅模型/视图编程概述。

../_images/simpletreemodel-example.png

Qt的模型/视图架构提供了一个标准方式,允许视图通过使用数据的一个抽象模型来操纵数据源中的信息,简化并标准化数据访问的方式。简单模型将数据表示为一个项表,并允许视图通过一个基于索引的系统访问这些数据。更一般来说,模型可以通过允许每个项作用于子项表中的一个父项来以树结构的形式表示数据。

在尝试实现树形模型之前,考虑数据是否由外部源提供,或者是否将在模型内部维护,是值得的。在此示例中,我们将实现一个内部结构来存储数据,而不是讨论如何包装外部源的数据。

设计和概念#

我们用来表示数据结构的结构采用了从TreeItem对象构建的树的形式。每个TreeItem代表树视图中的一项,并包含几个数据列。

treemodel-structure1

简单树型模型结构

数据在模型内部使用通过指针链接的TreeItem对象存储。通常,每个TreeItem有一个父项,并且可以有多个子项。然而,树结构中的根项没有父项,并且在模型外部从不被引用。

每个TreeItem包含关于其在树结构中位置的信息;它可以返回其父项和其行号。这种信息便于获取使得实现模型更加简单。

由于树视图中的每个项通常包含多个数据列(在本例中是一个标题和一个摘要),自然地,将此信息存储在每个项中。为了简单起见,我们将使用一个QVariant对象的列表来存储项中每个列的数据。

使用基于指针的树结构意味着,当我们向视图传递一个模型索引时,我们可以在索引中记录对应项的地址(参见QAbstractItemModel::createIndex()),并在以后使用QModelIndex::internalPointer()检索它。这使得编写模型更容易,并确保所有引用同一项的模型索引指向相同的数据指针。

在适当的数据结构到位的情况下,我们可以使用极少的额外代码构建一个树模型,为其他组件提供模型索引和数据。

TreeItem 类定义#

TreeItem 类定义如下

class TreeItem():

# public
    TreeItem = explicit(QVariantList data, TreeItem parentItem = None)
    def appendChild(andchild):
    child = TreeItem(int row)
    childCount = int()
    columnCount = int()
    data = QVariant(int column)
    row = int()
    parentItem = TreeItem()
# private
    std.vector<std.unique_ptr<TreeItem>> m_childItems
    m_itemData = QVariantList()
    m_parentItem = TreeItem()

这是一个基本的 C++ 类。它不是从 QObject 继承,也不提供信号和槽。它用于保存一个包含列数据及其在树结构中位置信息的 QVariants 列表。函数提供以下功能

  • appendChildItem() 用于在模型首次构建时添加数据,在正常使用期间不使用。

  • child()childCount() 函数允许模型获取任何子项的信息。

  • columnCount() 提供与项目相关联的列数的有关信息,可以使用 data() 函数获取每列中的数据。

  • row()parent() 函数用于获取项目的行数及其父项。

父项和列数据存储在私有成员变量 parentItemitemData 中。变量 childItems 包含指向项目自己子项的指针列表。

TreeItem 类实现#

构造函数只用于记录项目的父项以及每列相关联的数据。

def __init__(self, data, parent):
    : m_itemData(std.move(data)), m_parentItem(parent)
{}

每个属于该项目的子项的指针将以 std::unique_ptr 存储在私有成员变量 childItems 中。当调用类的析构函数时,将自动删除子项以确保内存重用。

由于每个子项都是在模型初始加载数据时构建的,因此添加子项的函数非常简单。

def appendChild(self, andchild):

    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 and row < childCount() if m_childItems.at(row).get() else None

通过 childCount() 可以找到持有的子项数量。

def childCount(self):

    return int(m_childItems.size())

TreeModel 使用此函数来确定给定父项存在的行数数量。

row() 函数报告项目在其父项的项目列表中的位置。

def row(self):

    if m_parentItem == None:
        return 0
    it = std::find_if(m_parentItem.m_childItems.cbegin(), m_parentItem.m_childItems.cend(),
                                 [self](std.unique_ptr<TreeItem> treeItem) {
                                     return treeItem.get() == self
                                 })
    if it != m_parentItem.m_childItems.cend():
        def distance(self, m_parentItem.m_childItems.cbegin(), it):
    Q_ASSERT(False) # should not happen
    return -1

请注意,尽管根项(没有父项)自动分配了行号为 0,但该信息永远不会被模型使用。

通过 columnCount() 函数返回项目的数据列数,这是最简单不过的了。

def columnCount(self):

    return int(m_itemData.count())

列数据由 data() 函数返回。我们使用 QList::value() 便利函数,它会检查边界并在边界被违反时返回一个默认构造的 QVariant

def data(self, int column):

    return m_itemData.value(column)

使用 parent() 函数找到项的父级

TreeItem TreeItem.parentItem()

    return m_parentItem

请注意,由于模型中的根项不会有父级,因此在这种情况下此函数将返回零。在实现 TreeModel::parent() 函数时,我们需要确保模型正确处理这种情况。

TreeModel类定义#

TreeModel 类定义如下

class TreeModel(QAbstractItemModel):

    Q_OBJECT
# public
    Q_DISABLE_COPY_MOVE(TreeModel)
    TreeModel = explicit(QString data, QObject parent = None)
    ~TreeModel() override
    QVariant data(QModelIndex index, int role) override
    Qt.ItemFlags flags(QModelIndex index) override
    QVariant headerData(int section, Qt.Orientation orientation,
                        role = Qt.DisplayRole) override()
    QModelIndex index(int row, int column,
                      QModelIndex parent = {}) override
    QModelIndex parent(QModelIndex index) override
    int rowCount(QModelIndex parent = {}) override
    int columnCount(QModelIndex parent = {}) override
# private
    def setupModelData(lines, parent):
    std.unique_ptr<TreeItem> rootItem

此类类似于大多数其他提供只读模型的 QAbstractItemModel 子类。只有构造函数的形式和 setupModelData() 函数对于此模型是特定的。此外,我们还提供一个析构函数,在模型被销毁时进行清理。

TreeModel类实现#

为了简单起见,模型不允许编辑其数据。因此,构造函数接受一个包含模型将与视图和代理共享的数据的参数

def __init__(self, data, parent):
    super().__init__(parent)
    , rootItem(std.make_unique<TreeItem>(QVariantList{tr("Title"), tr("Summary")}))

    setupModelData(QStringView{data}.split('\n'), rootItem.get())

构造函数负责为模型创建一个根项。此项仅包含垂直表头数据以提高便利性。我们还使用它来引用包含模型数据的内部数据结构,并且它用于表示模型中顶级项的想象中父级。根项使用 std::unique_ptr 进行管理,以确保模型被删除时整个项树被删除。

模型内部数据结构通过 setupModelData() 函数填充项。我们将在本文档末尾单独检查此函数。

析构函数确保在模型销毁时删除根项及其所有后代。由于根项存储在 unique_ptr 中,因此这是自动完成的。

TreeModel.~TreeModel() = default

由于我们无法在模型构造和设置之后添加数据,这简化了内部项树的管理方式。

模型必须实现一个 index() 函数,以为视图和代理提供访问数据时使用的索引。在引用其行和列编号以及父模型索引时,为其他组件创建索引。如果指定的父索引无效,则模型必须返回与模型中顶层项对应的索引。

当提供模型索引时,我们首先检查它是否有效。如果不是,我们假设正在引用一个顶层项;否则,我们使用其内部指针()函数从模型索引中获取数据指针,并使用它来引用一个 TreeItem 对象。请注意,我们将构建的所有模型索引都将包含对现有 TreeItem 的指针,因此我们可以保证我们接收到的任何有效模型索引都将包含有效的数据指针。

def index(self, int row, int column, QModelIndex parent):

    if not hasIndex(row, column, parent):
        return {}
    parentItem = parent.isValid()
        ? TreeItem(parent.internalPointer())

    if auto childItem = parentItem.child(row):
        return createIndex(row, column, childItem)
    return {}

由于此函数的行和列参数指代对应父项的子项,我们使用 TreeItem::child() 函数来获取项。使用 createIndex() 函数创建一个要返回的模型索引。我们指定行号和列号以及项本身的指针。模型索引可以用于以后获取项的数据。

TreeItem 对象的定义方式使得编写 parent() 函数变得简单

def parent(self, QModelIndex index):

    if not index.isValid():
        return {}
    childItem = TreeItem(index.internalPointer())
    parentItem = childItem.parentItem()
    return parentItem != rootItem.get()
        ? createIndex(parentItem.row(), 0, parentItem) : QModelIndex{}

我们只需要确保永远不会返回与根项对应的模型索引。为了与实现 index() 函数的方式保持一致,我们对模型中任何顶级项的父项返回一个无效的模型索引。

当我们创建一个要返回的模型索引时,必须在项其自身父项中指定其行号和列号。我们可以很容易地使用 TreeItem::row() 函数找到行号,但我们遵循一个将父项的列号指定为0的传统。与 index() 函数相同,我们使用 createIndex() 创建模型索引。

rowCount() 函数简单地返回与给定模型索引对应的 TreeItem 的子项数量,或者如果指定了无效索引,则返回顶级项的数量。

def rowCount(self, QModelIndex parent):

    if parent.column() > 0:
        return 0
    parentItem = parent.isValid()
        ? TreeItem(parent.internalPointer())

    return parentItem.childCount()

由于每个项都管理自己的列数据,所以 columnCount() 函数必须调用项的自身 columnCount() 函数来确定给定模型索引有多少列。与 rowCount() 函数一样,如果指定了无效的模型索引,返回的列数将从根项确定。

def columnCount(self, QModelIndex parent):

    if parent.isValid():
        return TreeItem(parent.internalPointer()).columnCount()
    return rootItem.columnCount()

数据通过 data() 从模型获取。由于项管理自己的列,我们需要使用列号通过 TreeItem::data() 函数检索数据

def data(self, QModelIndex index, int role):

    if not index.isValid() or role not = Qt.DisplayRole:
        return {}
    item = TreeItem(index.internalPointer())
    return item.data(index.column())

请注意,我们在此实现中仅支持 DisplayRole,并且对于无效的模型索引,我们还返回无效的 QVariant 对象。

我们使用 flags() 函数确保视图知道模型是只读的

Qt.ItemFlags TreeModel.flags(QModelIndex index)

    return index.isValid()
        ? QAbstractItemModel.flags(index) : Qt.ItemFlags(Qt.NoItemFlags)

headerData() 函数返回我们方便地存储在根项中的数据

QVariant TreeModel.headerData(int section, Qt.Orientation orientation,
                               int role)

    orientation = = Qt.Horizontal and role == Qt.DisplayRole
        ? rootItem.data(section) : QVariant{}

这些信息可以通过不同的方式提供:要么在构造函数中指定,要么将它们硬编码到 headerData() 函数中。

在模型中设置数据#

我们使用 setupModelData() 函数来设置模型中的初始数据。此函数解析一个文本文件,提取文本字符串以在模型中使用,并创建记录数据和整体模型结构的项对象。显然,该函数的工作方式非常具体于本模型。我们提供了以下关于其行为描述,并请读者参考示例代码以获取更多信息。

我们以以下格式的文本文件开始

Getting Started                         How to familiarize yourself with Qt Designer
    Launching Designer                  Running the Qt Designer application
    The User Interface                  How to interact with Qt Designer
    ...
...

使用以下两个规则处理文本文件

  • 对于每一行上的每一对字符串,在树结构中创建一个项(或节点),并将每个字符串放置在项的数据列中。

  • 当一行上的第一个字符串相对于上一行的第一个字符串进行缩进时,使该项成为之前创建的项的子项。

为了确保模型能够正确工作,只需创建具有正确数据和父项的 TreeItem 实例即可。

测试模型#

正确实现项模型可能具有挑战性。Qt 测试模块中的 QAbstractItemModelTester 类检查模型一致性,如模型索引创建和父子关系。

您可以通过将模型实例传递给类构造函数来测试您的模型,例如,作为 Qt 单元测试的一部分

class TestSimpleTreeModel(QObject):

    Q_OBJECT
# private slots
    def testTreeModel():

def testTreeModel(self):

   expr auto fileName = ":/default.txt"
    file = QFile(fileName)
    QVERIFY2(file.open(QIODevice.ReadOnly | QIODevice.Text),
             qPrintable(fileName + " cannot be opened: " + file.errorString()))
    model = TreeModel(QString.fromUtf8(file.readAll()))
    tester = QAbstractItemModelTester(model)

QTEST_APPLESS_MAIN(TestSimpleTreeModel)
from test.moc import *

要创建一个可以通过 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"
    PREFIX
        "/"
    FILES
        ${simpletreemodel_resource_files}
)

add_test(NAME simpletreemodel_tester
         COMMAND simpletreemodel_tester)

示例项目 @ code.qt.io