模型/视图编程
模型/视图编程简介
Qt 包含一组项目视图类,这些类采用模型/视图架构来管理数据与其向用户呈现方式之间的关系。该架构所引入的功能分离,使开发人员能够更灵活地自定义项目的呈现方式,并提供了一个标准的模型接口,从而允许将各种数据源与现有的项目视图结合使用。 本文将简要介绍模型/视图范式,概述相关概念,并阐述项目视图系统的架构。文中将详细说明该架构中的各个组件,并提供示例说明如何使用所提供的类。
模型/视图架构
模型-视图-控制器(MVC)是一种源自 Smalltalk 的设计模式,常用于构建用户界面。在《设计模式》一书中,Gamma 等人写道:
MVC 由三类对象组成。模型是应用程序对象,视图是其屏幕呈现形式,控制器则定义了用户界面响应用户输入的方式。在 MVC 出现之前,用户界面设计往往将这些对象混为一谈。MVC 将它们解耦,从而提高了灵活性和可重用性。
如果将视图和控制器对象合并,结果便是模型/视图架构。这种架构依然将数据的存储方式与向用户呈现的方式分离,但基于相同的原则提供了一个更简单的框架。 这种分离使得可以在多种不同的视图中显示相同的数据,并实现新型视图,而无需更改底层数据结构。为了灵活处理用户输入,我们引入了委托(delegate)的概念。在此框架中使用委托的优势在于,它允许自定义数据项的渲染和编辑方式。
| 模型/视图架构 模型与数据源进行通信,为架构中的其他组件提供接口。通信的性质取决于数据源的类型以及模型的实现方式。 视图从模型中获取模型索引;这些索引是数据项的引用。通过向数据源提供模型索引,视图可以从数据源中检索数据项。 在标准视图中,由委托负责渲染数据项。当数据项被编辑时,委托会使用模型索引直接与模型进行通信。 |
通常,模型/视图类可以分为上述三类:模型、视图和委托。这些组件均由抽象类定义,这些抽象类提供了通用接口,在某些情况下还提供了功能的默认实现。 抽象类旨在被子类继承,以提供其他组件所期望的完整功能集;这也允许编写专门化的组件。
模型、视图和委托通过信号与槽进行相互通信:
- 来自模型的信号会将数据源中数据的变更通知给视图。
- 来自视图的信号提供用户与所显示项目交互的相关信息。
- 来自委托的信号在编辑过程中用于向模型和视图通报编辑器的状态。
模型
所有项目模型均基于QAbstractItemModel 类。该类定义了一个接口,供视图和委托用于访问数据。数据本身不必存储在模型中;它可以保存在由单独类、文件、数据库或其他应用程序组件提供的数据结构或存储库中。
关于模型的基本概念在“模型类”一节中进行了介绍。
QAbstractItemModel 该类提供了一个足够灵活的数据接口,能够处理以表格、列表和树形结构形式呈现数据的视图。不过,在为列表和表格类数据结构实现新模型时,QAbstractListModel 和QAbstractTableModel 类是更好的起点,因为它们为常用函数提供了合适的默认实现。 可以对这些类分别进行子类化,以提供支持特定类型列表和表格的模型。
关于模型子类的创建过程,将在“创建新模型”一节中进行讨论。
Qt 提供了一些现成的模型,可用于处理数据项:
- QRangeModel 无需创建子类,即可将现有的 C++ 容器或任何可迭代的 C++ 范围适配到模型/视图框架中。
- QStringListModel 用于存储由QString 项组成的简单列表。
- QStandardItemModel 用于管理更复杂的项目树结构,其中每个项目均可包含任意数据。
- QFileSystemModel 提供有关本地文件系统中文件和目录的信息。
- QSqlQueryModel、QSqlTableModel 和QSqlRelationalTableModel 用于遵循模型/视图约定访问数据库。
如果这些标准模型无法满足您的需求,您可以继承QAbstractItemModel 、QAbstractListModel 或QAbstractTableModel 来创建自己的自定义模型。此外,如果您的数据已经存储在C++容器或范围中,QRangeModel 通常可以在不进行继承的情况下对其进行适配。
视图
针对不同类型的视图提供了完整的实现:QListView 显示项目列表,QTableView 以表格形式显示模型中的数据,而QTreeView 则以层次列表形式显示模型中的数据项。这些类均基于QAbstractItemView 抽象基类。尽管这些类是开箱即用的实现,但也可以通过继承它们来提供自定义视图。
关于可用视图的详细说明,请参阅“视图类”一节。
委托
QAbstractItemDelegate 是模型/视图框架中委托的抽象基类。默认的委托实现由QStyledItemDelegate 提供,Qt XML的标准视图将其用作默认委托。不过,QStyledItemDelegate 和QItemDelegate 是用于绘制视图中项目以及为其提供编辑器的独立替代方案。 它们之间的区别在于,QStyledItemDelegate 使用当前样式来绘制其项。因此,我们建议在实现自定义委托或使用 Qt 样式表时,将QStyledItemDelegate 作为基类。
关于委托的详细说明,请参见“委托类”一节。
排序
在模型/视图架构中,有两种处理排序的方法;选择哪种方法取决于您的底层模型。
如果您的模型可排序(即重写了QAbstractItemModel::sort() 函数),则QTableView 和QTreeView 都提供了 API,允许您通过编程方式对模型数据进行排序。 此外,您可以通过将QHeaderView::sortIndicatorChanged()信号分别连接到QTableView::sortByColumn()插槽或QTreeView::sortByColumn()插槽,来启用交互式排序(即允许用户通过点击视图的表头来对数据进行排序)。
如果您的模型没有所需的接口,或者您希望使用列表视图来呈现数据,另一种方法是使用代理模型,在将数据呈现到视图之前转换模型的结构。相关内容在“代理模型”一节中有详细介绍。
便利类
为了方便依赖于 Qt 基于项的项视图和表格类的应用程序,从标准视图类派生出了一系列便利类。这些类不打算被子类化。
此类类的示例包括QListWidget 、QTreeWidget 和QTableWidget 。
这些类比视图类灵活性更低,且无法与任意模型配合使用。除非您确实需要基于项的一组类,否则我们建议您采用模型/视图方法来处理项视图中的数据。
如果您希望在继续使用基于项的接口的同时,又能利用模型/视图方法提供的功能,请考虑将视图类(如QListView 、QTableView 和QTreeView )与QStandardItemModel 结合使用。
使用模型和视图
以下各节将说明如何在 Qt 中使用模型/视图模式。每节都包含一个示例,随后是一个展示如何创建新组件的章节。
Qt 中包含的两个模型
Qt XML 提供的两个标准模型是QStandardItemModel 和QFileSystemModel 。QStandardItemModel 是一个多功能模型,可用于表示列表、表格和树形视图所需的各种不同数据结构。 该模型还存储数据项。QFileSystemModel 是一个用于维护目录内容信息的模型。因此,它本身并不存储任何数据项,而是简单地表示本地文件系统上的文件和目录。
QFileSystemModel 提供了一个可直接使用的模型供实验,并可轻松配置以使用现有数据。通过此模型,我们可以演示如何设置模型以配合现成的视图使用,并探讨如何利用模型索引来操作数据。
在现有模型中使用视图
QListView 和QTreeView 类是与QFileSystemModel 配合使用最合适的视图。下面的示例将目录内容以树形视图显示,并在其旁边以列表视图显示相同信息。这两个视图共享用户的选中状态,因此选中的项目在两个视图中都会被高亮显示。

我们先设置一个QFileSystemModel 使其准备就绪,然后创建一些视图来显示目录内容。这展示了使用模型的最简单方法。模型的构建和使用都在一个main() 函数内完成:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSplitter *splitter = new QSplitter;
QFileSystemModel *model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());该模型配置为使用来自特定文件系统的数据。调用setRootPath() 会告知模型应向视图暴露文件系统中的哪个驱动器。
我们创建了两个视图,以便通过两种不同的方式查看模型中存储的项目:
QTreeView *tree = new QTreeView(splitter);
tree->setModel(model);
tree->setRootIndex(model->index(QDir::currentPath()));
QListView *list = new QListView(splitter);
list->setModel(model);
list->setRootIndex(model->index(QDir::currentPath()));视图的构建方式与其他小部件相同。要设置一个视图来显示模型中的项目,只需调用其setModel()函数,并将目录模型作为参数传入即可。我们通过在每个视图上调用setRootIndex()函数,并传入当前目录文件系统模型中的合适模型索引,来过滤模型提供的数据。
此处使用的index() 函数是QFileSystemModel 特有的;我们向其提供一个目录,它便返回一个模型索引。关于模型索引的详细说明,请参见“模型类”章节。
该函数的其余部分仅将视图显示在拆分器控件中,并运行应用程序的事件循环:
splitter->setWindowTitle("Two views onto the same file system model");
splitter->show();
return app.exec();
}在上述示例中,我们未提及如何处理项目的选中操作。关于此主题的详细内容,请参阅“处理项目视图中的选中操作”一节。
模型类
在探讨如何处理选择之前,了解模型/视图框架中使用的概念可能会对您有所帮助。
基本概念
在模型/视图架构中,模型提供了一个标准接口,视图和代理通过该接口访问数据。在 Qt 中,该标准接口由 `QAbstractItemModel ` 类定义。 无论数据项在底层数据结构中如何存储,QAbstractItemModel 的所有子类都将数据表示为包含数据项表的分层结构。视图使用此约定访问模型中的数据项,但在向用户呈现这些信息的方式上不受限制。
模型还会通过信号与插槽机制,将数据变更通知给任何关联的视图。
本节介绍了一些基本概念,这些概念是其他组件通过模型类访问数据项的核心。更高级的概念将在后续章节中讨论。
模型索引
为了确保数据的表示形式与访问方式保持分离,引入了模型索引的概念。通过模型可以获取的每一条信息都由一个模型索引表示。视图和委托使用这些索引来请求要显示的数据项。
因此,只需模型本身知道如何获取数据,而模型所管理的数据类型则可以定义得相当通用。模型索引包含指向创建它的模型的指针,这在处理多个模型时可避免混淆。
const QAbstractItemModel *model = index.model();模型索引为信息片段提供临时引用,可用于通过模型检索或修改数据。由于模型可能会不时重组其内部结构,模型索引可能会失效,因此不应将其存储。 如果需要对某条信息进行长期引用,则必须创建一个持久化模型索引。这将提供对该信息的引用,且该引用将由模型保持最新状态。临时模型索引由QModelIndex 类提供,持久化模型索引由QPersistentModelIndex 类提供。
要获取与某条数据对应的模型索引,必须向模型指定三个属性:行号、列号以及父项的模型索引。以下各节将详细描述和解释这些属性。
行与列
在最基本的形式下,模型可作为一张简单表格来访问,其中项目通过其行号和列号进行定位。这并不意味着底层数据存储在数组结构中;行号和列号的使用仅是一种约定,用于使组件之间能够相互通信。 我们可以通过向模型指定行号和列号来检索任何给定项的信息,并获得一个代表该项的索引:
QModelIndex index = model->index(row, column /*...*/);为列表和表格等简单单层数据结构提供接口的模型无需提供其他信息,但如上文代码所示,在获取模型索引时,我们需要提供更多信息。
| 行与列 该图展示了一个基本表格模型的表示,其中每个项目都由一行号和一列号的组合定位。我们将相关的行号和列号传递给模型,从而获得一个指向某个数据项的模型索引。 模型中的顶级项目总是通过将 |
项的父项
模型提供的、用于访问项目数据的表格式接口,在以表格或列表视图使用数据时最为理想;其行和列编号系统与视图显示项目的方式完全对应。然而,树形视图等结构则要求模型为其内部的项目提供更灵活的接口。 因此,每个项目也可以是另一个项目表的父项,这与树形视图中的顶级项目可以包含另一个项目列表的情况非常相似。
在请求模型项的索引时,必须提供有关该项父项的一些信息。在模型外部,引用项的唯一方式是通过模型索引,因此还必须提供父模型索引:
QModelIndex index = model->index(row, column, parent);| 父项、行和列 该图展示了一个树形模型的表示,其中每个项目通过父项、行号和列号来标识。 项目“A”和“C”在模型中被表示为顶级同级项目: 项目“A”有若干个子项。通过以下代码可获取项目“B”的模型索引: |
项的角色
模型中的项可以为其他组件扮演各种角色,从而允许根据不同情况提供不同类型的数据。例如,Qt::DisplayRole 用于访问一个字符串,该字符串可在视图中作为文本显示。通常,项包含适用于多个不同角色的数据,标准角色由Qt::ItemDataRole 定义。
我们可以向模型请求项的数据,方法是向其传递与该项对应的模型索引,并指定一个角色以获取所需的数据类型:
QVariant value = model->data(index, role);![]() | 项的角色 角色向模型指示所指代的数据类型。视图可以以不同方式显示这些角色,因此为每个角色提供适当的信息非常重要。 “创建新模型”一节将更详细地介绍角色的一些具体用法。 |
项目数据最常见的用途均由Qt::ItemDataRole 中定义的标准角色所涵盖。通过为每个角色提供适当的项目数据,模型可以向视图和委托者提供有关应如何向用户呈现项目的提示。不同类型的视图可以根据需要自由解释或忽略这些信息。此外,还可以为特定应用程序的目的定义额外的角色。
总结
- 模型索引以独立于任何底层数据结构的方式,向视图和委托提供有关模型中项目位置的信息。
- 项目通过其行号和列号,以及其父项目的模型索引来引用。
- 模型索引由模型应其他组件(如视图和委托)的请求而构建。
- 如果使用 `index()` 请求索引时为父项指定了有效的模型索引,则返回的索引将指向模型中该父项下方的某个项。获得的索引指向该项的子项。
- 如果使用index() 请求索引时为父项指定了无效的模型索引,则返回的索引将指向模型中该父项下的顶层项。
- role 方法能够区分与某个项关联的不同类型数据。
使用模型索引
为了演示如何使用模型索引从模型中检索数据,我们创建了一个不带视图的QFileSystemModel ,并在小部件中显示文件和目录的名称。虽然这并非使用模型的常规方式,但它展示了模型在处理模型索引时采用的惯例。
QFileSystemModel 加载是异步进行的,以最大限度地减少系统资源的占用。在处理该模型时,我们必须将这一点考虑在内。
我们按以下方式构建文件系统模型:
auto *model = new QFileSystemModel;
auto onDirectoryLoaded = [model, layout, &window](const QString &directory) {
QModelIndex parentIndex = model->index(directory);
const int numRows = model->rowCount(parentIndex);
for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
auto *label = new QLabel(text, &window);
layout->addWidget(label);
}
};
QObject::connect(model, &QFileSystemModel::directoryLoaded, onDirectoryLoaded);
model->setRootPath(QDir::currentPath());在此情况下,我们首先设置一个默认的QFileSystemModel 。我们将它的directoryLoaded(QString) 信号连接到一个lambda表达式中,在该表达式中,我们将利用该模型提供的index()函数的特定实现,获取该目录的父索引。
在该 lambda 函数中,我们通过rowCount() 函数确定模型中的行数。
为简化起见,我们仅关注模型第一列中的项目。我们依次检查每一行,获取每行第一个项目的模型索引,并读取模型中为该项目存储的数据。
for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);要获取模型索引,我们需要指定行号、列号(第一列为零)以及所需所有项的父项对应的适当模型索引。使用模型的data()函数检索存储在每个项中的文本。 我们指定模型索引和DisplayRole ,以字符串形式获取该项的数据。
最后,我们设置QFileSystemModel 的根路径,使其开始加载数据并触发Lambda函数。
上述示例演示了从模型中检索数据的基本原理:
- 可通过rowCount() 和columnCount() 获取模型的维度。这些函数通常需要指定父模型索引。
- 模型索引用于访问模型中的项目。指定项目时需要行、列和父模型索引。
- 要访问模型中的顶级项,请在调用
QModelIndex()时将父模型索引设为 null。 - 项目包含不同角色的数据。要获取特定角色的数据,必须同时向模型提供模型索引和角色。
进一步阅读
可以通过实现QAbstractItemModel 提供的标准接口来创建新模型。在“创建新模型”一节中,我们将通过创建一个方便且可直接使用的模型(用于存储字符串列表)来说明这一点。
视图类
概念
在模型/视图架构中,视图从模型中获取数据项,并将其呈现给用户。数据的呈现方式不必与模型提供的数据表示形式相同,甚至可能与用于存储数据项的底层数据结构完全不同。
内容与呈现的分离是通过使用QAbstractItemModel 提供的标准模型接口、QAbstractItemView 提供的标准视图接口,以及代表数据项的一般性模型索引来实现的。 视图通常负责管理从模型中获取的数据的整体布局。它们可以自行呈现单个数据项,也可以使用委托来处理呈现和编辑功能。
除了呈现数据外,视图还负责项目之间的导航以及项目选择的某些方面。视图还实现了基本的用户界面功能,例如上下文菜单和拖放操作。视图可以为项目提供默认的编辑功能,也可以通过委托来提供自定义编辑器。
视图可以在没有模型的情况下构建,但必须提供模型才能显示有用的信息。视图通过“选择”功能来跟踪用户所选的项目,这些选择可以针对每个视图单独维护,也可以在多个视图之间共享。
某些视图(如QTableView 和QTreeView )除了显示项目外,还会显示标题。这些功能同样由视图类QHeaderView 实现。标题通常访问与其所属视图相同的模型。它们使用QAbstractItemModel::headerData()函数从模型中检索数据,并通常以标签的形式显示标题信息。 可以通过继承QHeaderView 类来创建新的标题,从而为视图提供更专业的标签。
使用现有视图
Qt 提供了三个开箱即用的视图类,它们以大多数用户熟悉的方式呈现模型中的数据。QListView 可以将模型中的项目显示为简单的列表,或以经典图标视图的形式呈现。QTreeView 将模型中的项目以列表层次结构显示,从而能够以紧凑的方式呈现深度嵌套的结构。QTableView 将模型中的项目以表格形式呈现,其布局与电子表格应用程序非常相似。

上述标准视图的默认行为对于大多数应用程序而言已足够。它们提供了基本的编辑功能,并可进行自定义以满足更专业用户界面的需求。
使用模型
我们以之前创建的字符串列表模型作为示例模型,为其设置一些数据,并构建一个视图来显示模型的内容。所有这些操作都可以在一个函数内完成:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// Unindented for quoting purposes:
QStringList numbers;
numbers << "One" << "Two" << "Three" << "Four" << "Five";
QAbstractItemModel *model = new StringListModel(numbers);请注意,StringListModel 被声明为QAbstractItemModel 。这使我们能够使用该模型的抽象接口,并确保即使将字符串列表模型替换为其他模型,代码仍能正常运行。
QListView 提供的列表视图足以展示字符串列表模型中的项目。我们通过以下几行代码构建视图并设置模型:
视图以常规方式显示:
view->show();
return app.exec();
}视图通过模型的接口访问数据,从而呈现模型的内容。当用户尝试编辑某项时,视图会使用默认委托来提供编辑控件。

上图展示了QListView 如何呈现字符串列表模型中的数据。由于该模型支持编辑,视图会自动通过默认委托允许用户编辑列表中的每个项目。
使用模型的多个视图
要为同一个模型提供多个视图,只需为每个视图设置相同的模型即可。在下面的代码中,我们创建了两个表格视图,每个视图都使用为本示例创建的同一个简单表格模型:
QTableView *firstTableView = new QTableView;
QTableView *secondTableView = new QTableView;
firstTableView->setModel(model);
secondTableView->setModel(model);在模型/视图架构中使用信号和插槽,意味着对模型的更改可以传播到所有关联的视图中,从而确保无论使用哪个视图,我们都能始终访问相同的数据。

上图展示了针对同一模型的两个不同视图,每个视图中都包含若干已选中的项目。尽管模型中的数据在各个视图中显示一致,但每个视图都维护着自己的内部选择模型。这在某些情况下可能很有用,但对于许多应用程序而言,共享的选择模型更为理想。
处理项目选择
QItemSelectionModel 类提供了处理视图内项目选择的机制。所有标准视图默认都会构建自己的选择模型,并以常规方式与之交互。 可以通过selectionModel()函数获取视图正在使用的选择模型,并可通过setSelectionModel()指定替代的选择模型。当需要为同一模型数据提供多个一致的视图时,控制视图所用选择模型的能力非常有用。
通常情况下,除非您正在子类化模型或视图,否则无需直接操作选择集的内容。不过,如有需要,仍可访问选择模型的接口,相关内容将在“处理项目视图中的选择”中进行探讨。
在视图之间共享选择
尽管视图类默认提供自己的选择模型非常方便,但当我们在同一模型上使用多个视图时,通常希望模型的数据和用户的选中状态在所有视图中都能保持一致。 由于视图类允许替换其内部的选择模型,我们可以通过以下代码实现视图之间的统一选择:
secondTableView->setSelectionModel(firstTableView->selectionModel());第二个视图被赋予了第一个视图的选择模型。此时,两个视图均基于同一选择模型运行,从而确保数据和所选项目保持同步。

在上文示例中,使用了两个同类型的视图来显示同一模型的数据。但是,如果使用了两种不同类型的视图,则每个视图中选中项的呈现方式可能会大不相同;例如,表格视图中连续的选中区域在树形视图中可能会被呈现为一组零散的高亮项目。
委托类
概念
与模型-视图-控制器(MVC)模式不同,模型/视图设计中并不包含一个完全独立的组件来管理与用户的交互。 通常,视图负责向用户呈现模型数据,并处理用户输入。为了在获取用户输入的方式上提供一定的灵活性,交互操作由委托来执行。这些组件提供输入功能,同时负责在某些视图中渲染单个项目。控制委托的标准接口在QAbstractItemDelegate 类中定义。
委托应通过实现paint() 和sizeHint() 函数来自行渲染其内容。不过,基于简单控件的委托可以继承自QStyledItemDelegate 而不是QAbstractItemDelegate ,从而利用这些函数的默认实现。
委托的编辑器可以通过使用小部件来管理编辑过程,或者通过直接处理事件来实现。第一种方法将在本节后文中介绍。
使用现有的委托
QStyledItemDelegate QListView QTableView QTreeView
所有标准角色均由标准视图使用的默认委托处理。这些角色的解释方式在QStyledItemDelegate 文档中进行了说明。
视图所使用的委托由itemDelegate() 函数返回。setItemDelegate() 函数允许您为标准视图安装自定义委托,在为自定义视图设置委托时必须使用此函数。
一个简单的委托
此处实现的委托使用QSpinBox 提供编辑功能,主要适用于显示整数的模型。虽然我们为此目的设置了一个基于整数的自定义表格模型,但其实完全可以使用QStandardItemModel 代替,因为自定义委托控制着数据输入。 我们构建一个表格视图来显示模型的内容,该视图将使用自定义委托进行编辑。

我们从QStyledItemDelegate 继承该委托,因为我们不想编写自定义显示函数。不过,我们仍需提供用于管理编辑器控件的函数:
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = nullptr);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
SpinBoxDelegate::SpinBoxDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}请注意,在构建委托时不会设置任何编辑器控件。我们仅在需要时才创建编辑器控件。
提供编辑器
在此示例中,当表格视图需要提供编辑器时,它会请求委托提供一个适合当前被修改项的编辑器控件。createEditor() 函数会向委托提供其设置合适控件所需的一切信息:
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &/* option */,
const QModelIndex &/* index */) const
{
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false);
editor->setMinimum(0);
editor->setMaximum(100);
return editor;
}请注意,我们无需保留对编辑器控件的指针,因为当该控件不再需要时,视图会负责将其销毁。
我们在编辑器上安装委托的默认事件过滤器,以确保其提供用户所期望的标准编辑快捷键。还可以向编辑器添加额外的快捷键以实现更复杂的行为;相关内容将在“编辑提示”一节中讨论。
视图通过调用我们稍后为此目的定义的函数,确保编辑器的数据和几何属性设置正确。我们可以根据视图提供的模型索引创建不同的编辑器。例如,如果我们有一列整数和一列字符串,则可以根据正在编辑的列,返回QSpinBox 或QLineEdit 。
委托必须提供一个函数,用于将模型数据复制到编辑器中。在此示例中,我们读取存储在display role 中的数据,并据此设置旋转框中的值。
void SpinBoxDelegate::setEditorData(QWidget *editor,
const QModelIndex &index) const
{
int value = index.data(Qt::EditRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(value);
}在此示例中,我们知道编辑器控件是一个旋转框,但我们也可以为模型中的不同数据类型提供不同的编辑器;在这种情况下,在访问其成员函数之前,我们需要将控件强制转换为相应的类型。
将数据提交至模型
当用户完成对旋转框中值的编辑后,视图会通过调用setModelData()函数,请求委托将编辑后的值存储到模型中。
void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const
{
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->interpretText();
int value = spinBox->value();
model->setData(index, value, Qt::EditRole);
}由于视图负责管理委托的编辑器控件,我们只需将编辑器提供的内容更新到模型中即可。在这种情况下,我们会确保下拉列表框的内容是最新的,并使用指定的索引,将其包含的值更新到模型中。
标准的QStyledItemDelegate 类会在编辑完成时通过发出closeEditor()信号来通知视图。视图会确保编辑器控件被关闭并销毁。在本示例中,我们仅提供了简单的编辑功能,因此无需发出该信号。
所有数据操作均通过QAbstractItemModel 提供的接口进行。这使得委托在很大程度上独立于其操作的数据类型,但为了使用某些类型的编辑器控件,必须做出一些假设。 在此示例中,我们假设模型始终包含整数值,但我们仍然可以将此委托用于不同类型的模型,因为 `QVariant ` 针对意外数据提供了合理的默认值。
更新编辑器的几何形状
管理编辑器的几何属性是委托的职责。在创建编辑器时,以及当项目在视图中的大小或位置发生变化时,都必须设置几何属性。幸运的是,视图会在view option 对象中提供所有必要的几何信息。
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &/* index */) const
{
editor->setGeometry(option.rect);
}在此情况下,我们只需使用视图选项在项目矩形中提供的几何信息。对于渲染包含多个元素的项目,委托不会直接使用项目矩形,而是会根据项目中其他元素的位置来定位编辑器。
编辑提示
编辑完成后,委托应向其他组件提供有关编辑过程结果的提示,并提供有助于后续编辑操作的提示。这通过发出带有适当提示的closeEditor()信号来实现。这一点由我们在旋转框构造时安装的默认QStyledItemDelegate 事件过滤器负责处理。
可以调整旋转框的行为,使其更符合用户习惯。在QStyledItemDelegate 提供的默认事件过滤器中,如果用户点击Return 以确认旋转框中的选择,委托程序会将该值提交到模型并关闭旋转框。 我们可以通过在旋转框上安装自定义事件过滤器来更改此行为,并提供符合我们需求的编辑提示;例如,我们可以触发closeEditor()并附带EditNextItem 提示,从而自动开始编辑视图中的下一个项目。
另一种无需使用事件过滤器的方法是提供自定义编辑器控件,为方便起见,可以继承自QSpinBox 。 这种替代方法虽然需要编写额外的代码,但能让我们更好地控制编辑器小部件的行为。如果需要自定义标准 Qt 编辑器小部件的行为,通常在委托中安装一个事件过滤器会更简单。
委托并不一定非要发出这些提示,但那些不发出提示的委托将难以与应用程序集成,其可用性也会低于那些通过发出提示来支持常见编辑操作的委托。
在项视图中处理选区
概念
项目视图类中使用的选择模型基于模型/视图架构的特性,对选择功能进行了通用描述。尽管用于操作选择的标准类已足以满足现有项目视图的需求,但该选择模型允许您创建专门的选择模型,以满足您自己的项目模型和视图的要求。
关于视图中选中项的信息存储在QItemSelectionModel 类的实例中。该类维护单个模型中各项的模型索引,且与任何视图均无关联。由于一个模型可以有多个视图,因此可以在视图之间共享选中项,从而使应用程序能够以一致的方式显示多个视图。
选择由选择范围组成。这些范围通过仅记录每个所选项目范围的起始和结束模型索引,高效地维护了大量项目选择的信息。对于非连续的项目选择,可通过使用多个选择范围来描述该选择。
选择应用于由选择模型持有的模型索引集合。最近应用的项目选择称为当前选择。即使在应用之后,也可以通过使用某些类型的选择命令来修改该选择的效果。本节后面将对此进行讨论。
当前项目与所选项目
在视图中,始终存在“当前项目”和“选中项目”——这是两种独立的状态。一个项目可以同时作为当前项目和选中项目。视图负责确保始终存在当前项目,因为例如键盘导航就需要当前项目。
下表重点说明了当前项目与选定项目之间的区别。
| 当前项目 | 选中项 |
|---|---|
| 当前项目只能有一个。 | 可以选择多个项目。 |
| 当前项目将通过键盘导航或鼠标点击进行更改。 | 当用户与项目交互时,项目的选中状态会根据几种预定义模式(例如单选、多选等)进行设置或取消。 |
| 如果按下编辑键F2 ,或者双击项目(前提是已启用编辑功能),则会编辑当前项目。 | 当前项目可与锚点结合使用,以指定应选中或取消选中的范围(或两者的组合)。 |
| 当前项目由焦点矩形指示。 | 已选中的项目由选中矩形标识。 |
在操作选择时,通常将QItemSelectionModel 视为项目模型中所有项目的选择状态记录会很有帮助。一旦建立了选择模型,就可以对项目集合进行选择、取消选择或切换其选择状态,而无需知道哪些项目已被选中。 所有已选项目的索引可随时检索,并且可以通过信号和插槽机制将选择模型的变更通知给其他组件。
使用选择模型
标准视图类提供了可在大多数应用程序中使用的默认选择模型。可以通过视图的selectionModel()函数获取属于该视图的选择模型,并通过setSelectionModel()在多个视图之间共享,因此通常无需构建新的选择模型。
通过向QItemSelection 提供一个模型以及一对模型索引,即可创建一个选择。该函数会使用这些索引来引用给定模型中的项目,并将它们解释为所选项目块中的左上角和右下角项目。 要将选区应用于模型中的项目,需要将该选区提交给一个选区模型;这可以通过多种方式实现,每种方式对选区模型中已存在的选区都会产生不同的影响。
选择项目
为了演示选择功能的一些主要特性,我们创建了一个总计包含 32 个项目的自定义表格模型实例,并打开一个表格视图来显示其数据:
TableModel *model = new TableModel(8, 4, &app);
QTableView *table = new QTableView(0);
table->setModel(model);
QItemSelectionModel *selectionModel = table->selectionModel();获取表格视图的默认选择模型以备后用。我们不会修改模型中的任何项目,而是选择几个项目,以便视图将其显示在表格的左上角。为此,我们需要获取该选择区域中左上角和右下角项目对应的模型索引:
QModelIndex topLeft;
QModelIndex bottomRight;
topLeft = model->index(0, 0, QModelIndex());
bottomRight = model->index(5, 2, QModelIndex());为了在模型中选中这些项目,并在表格视图中看到相应的变化,我们需要构建一个选择对象,然后将其应用到选择模型上:
QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);通过由selection flags 组合定义的命令,将选择应用到选择模型中。在此情况下,所使用的标志会导致选择对象中记录的项被纳入选择模型,无论它们之前的状态如何。最终的选择结果将由视图显示出来。

可以通过由选择标志定义的各种操作来修改项目的选择。这些操作产生的选择可能具有复杂的结构,但选择模型能高效地表示它。关于使用不同的选择标志来操作所选项目的内容,将在探讨如何更新选择时进行说明。
读取选择状态
可以使用selectedIndexes()函数读取存储在选择模型中的模型索引。该函数返回一个未排序的模型索引列表,只要我们知道这些索引对应的是哪个模型,就可以对其进行迭代:
const QModelIndexList indexes = selectionModel->selectedIndexes();
for (const QModelIndex &index : indexes) {
QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
model->setData(index, text);
}上述代码使用基于范围的 for 循环来遍历并修改与选择模型返回的索引相对应的项目。
选择模型会发出信号来指示选择状态的变化。这些信号会通知其他组件,既包括整个选择范围的变化,也包括项目模型中当前焦点项的变化。 我们可以将selectionChanged()信号连接到一个插槽,并在选择状态发生变化时,检查模型中被选中或被取消选中的项目。该插槽会接收两个QItemSelection 对象:一个包含对应于新选中项目的索引列表;另一个包含对应于新被取消选中项目的索引。
在下面的代码中,我们提供了一个接收selectionChanged()信号的槽,将选中项的内容替换为字符串,并清空未选中项的内容。
void MainWindow::updateSelection(const QItemSelection &selected,
const QItemSelection &deselected)
{
QModelIndexList items = selected.indexes();
for (const QModelIndex &index : std::as_const(items)) {
QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
model->setData(index, text);
}
items = deselected.indexes();
for (const QModelIndex &index : std::as_const(items)) {
model->setData(index, QString());
}
}我们可以将currentChanged()信号连接到一个槽中,该槽会接收两个模型索引作为参数,以此来跟踪当前聚焦的项目。这两个索引分别对应之前聚焦的项目和当前聚焦的项目。
在下面的代码中,我们提供了一个槽,该槽接收currentChanged()信号,并利用提供的信息更新QMainWindow 的状态栏:
void MainWindow::changeCurrent(const QModelIndex ¤t,
const QModelIndex &previous)
{
statusBar()->showMessage(
tr("Moved from (%1,%2) to (%3,%4)")
.arg(previous.row()).arg(previous.column())
.arg(current.row()).arg(current.column()));
}借助这些信号,监控用户的选中操作非常简单,但我们也可以直接更新选中模型。
更新选择
选择命令由一组选择标志组合提供,这些标志在QItemSelectionModel::SelectionFlag 中进行了定义。每个选择标志都会告知选择模型,当调用select()中的任一函数时,应如何更新其内部已选项目记录。 最常用的标志是Select 标志,它指示选择模型将指定的项目记录为已选中。Toggle 标志会使选择模型反转指定项目的状态:选中任何未选中的项目,并取消选中任何当前已选中的项目。Deselect 标志则会取消选中所有指定的项目。
选择模型中的单个项目是通过创建项目选择集,并将其应用到选择模型中来更新的。在下面的代码中,我们使用Toggle 命令将第二个项目选择集应用到上文所示的表模型中,以反转所给项目的选择状态。
QItemSelection toggleSelection;
topLeft = model->index(2, 1, QModelIndex());
bottomRight = model->index(7, 3, QModelIndex());
toggleSelection.select(topLeft, bottomRight);
selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);该操作的结果将显示在表格视图中,这为我们提供了一种直观查看操作效果的便捷方式:

默认情况下,选择命令仅对模型索引指定的单个项目起作用。 不过,用于描述选择命令的标志可以与其他标志结合使用,从而修改整行或整列。例如,如果调用select()时只指定一个索引,但将命令设置为Select 和Rows 的组合,则包含该项目的整行都会被选中。以下代码演示了Rows 和Columns 标志的使用:
QItemSelection columnSelection;
topLeft = model->index(0, 1, QModelIndex());
bottomRight = model->index(0, 2, QModelIndex());
columnSelection.select(topLeft, bottomRight);
selectionModel->select(columnSelection,
QItemSelectionModel::Select | QItemSelectionModel::Columns);
QItemSelection rowSelection;
topLeft = model->index(0, 0, QModelIndex());
bottomRight = model->index(1, 0, QModelIndex());
rowSelection.select(topLeft, bottomRight);
selectionModel->select(rowSelection,
QItemSelectionModel::Select | QItemSelectionModel::Rows);尽管仅向选择模型提供了四个索引,但通过使用Columns 和Rows 选择标志,最终选中了两列和两行。下图展示了这两次选择的结果:

在示例模型上执行的命令均涉及在模型中累积选定项目。此外,还可以清除选定项,或用新的选定项替换当前选定项。
若要用新选择集替换当前选择集,请将其他选择标志与Current 标志结合使用。 使用此标志的命令会指示选择模型将其当前的模型索引集合替换为调用 `select()` 时指定的索引。在开始添加新选择之前,若要清除所有选择,请将其他选择标志与 `Clear ` 标志结合使用。这将重置选择模型中的模型索引集合。
选择模型中的所有项目
要选择模型中的所有项目,必须为模型的每个层级创建一个覆盖该层级所有项目的选择。我们通过检索给定父索引对应的左上角和右下角项目的索引来实现这一点:
QModelIndex topLeft = model->index(0, 0, parent);
QModelIndex bottomRight = model->index(model->rowCount(parent)-1,
model->columnCount(parent)-1, parent);利用这些索引和模型构建一个选择集。随后,在选择模型中选中相应的项目:
QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);需要对模型中的所有级别执行此操作。对于顶级项目,我们将按照常规方式定义父索引:
QModelIndex parent = QModelIndex();对于分层模型,使用hasChildren() 函数来确定给定项是否是另一层级项的父项。
创建新模型
模型与视图组件之间功能的分离,使得创建的模型能够充分利用现有的视图。这种方法使我们能够使用标准的图形用户界面组件(例如QListView 、QTableView 和QTreeView )来呈现来自各种来源的数据。
QAbstractItemModel 类提供了一个足够灵活的接口,能够支持将信息组织为层级结构的数据源,从而允许以某种方式对数据进行插入、删除、修改或排序。它还支持拖放操作。
QAbstractListModel 和QAbstractTableModel 类为更简单的非层次化数据结构提供了接口支持,作为构建简单列表和表格模型的起点,使用起来更为便捷。
在本节中,我们将创建一个简单的只读模型,以探索模型/视图架构的基本原理。在本节后半部分,我们将对这个简单模型进行调整,以便用户能够修改项目。
有关更复杂模型的示例,请参阅“简单树模型”示例。
关于QAbstractItemModel 子类的要求,在《模型子类化参考》文档中有更详细的描述。
设计模型
为现有数据结构创建新模型时,必须考虑应使用何种类型的模型来为数据提供接口。 如果数据已经保存在 C++ 容器中,或者保存在任何模拟可迭代 C++ 范围的类型中,只要输入范围及其元素类型受到支持,QRangeModel 便可以在无需任何子类化的情况下提供一个合适的模型。
如果现成的模型不适用,且数据结构可以表示为项目的列表或表,则可以继承QAbstractListModel 或QAbstractTableModel ,因为这些类为许多函数提供了合适的默认实现。
但是,如果底层数据结构只能通过分层树结构来表示,则必须继承QAbstractItemModel 。在“简单树模型”示例中采用了这种方法。
在本节中,我们将基于一个字符串列表实现一个简单模型,因此 `QAbstractListModel ` 提供了理想的基类作为构建基础。
无论底层数据结构采用何种形式,在专用模型中,通常最好对标准的QAbstractItemModel API进行补充,使其能够更自然地访问底层数据结构。这使得向模型中填充数据变得更加容易,同时仍允许其他通用的模型/视图组件使用标准API与其交互。 下文所述的模型正是为此目的提供了一个自定义构造函数。
一个只读示例模型
此处实现的模型是一个基于标准QStringListModel 类的简单、非层次结构的只读数据模型。它以QStringList 作为内部数据源,并且仅实现了使模型能够正常运行所需的必要功能。 为了简化实现,我们继承了QAbstractListModel 类,因为它为列表模型定义了合理的默认行为,并且其暴露的接口比QAbstractItemModel 类更简单。
在实现模型时,需要牢记QAbstractItemModel 本身并不存储任何数据,它仅仅提供了一个供视图访问数据的接口。对于最基本的只读模型,由于接口的大部分方法已有默认实现,因此只需实现少数几个函数即可。类声明如下:
class StringListModel : public QAbstractListModel
{
Q_OBJECT
public:
StringListModel(const QStringList &strings, QObject *parent = nullptr)
: QAbstractListModel(parent), stringList(strings) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
private:
QStringList stringList;
};除了模型的构造函数外,我们只需实现两个函数:rowCount() 返回模型中的行数,data() 返回与指定模型索引相对应的数据项。
行为规范的模型还会实现headerData(),以便树形视图和表格视图能在其标题栏中显示相关信息。
请注意,这是一个非层次结构模型,因此我们无需担心父子关系。如果我们的模型是层次结构的,我们还必须实现index() 和parent() 函数。
该字符串列表在内部存储于私有成员变量stringList 中。
模型的维度
我们希望模型的行数与字符串列表中的字符串数量保持一致。基于这一考虑,我们实现了rowCount() 函数:
int StringListModel::rowCount(const QModelIndex &parent) const
{
return stringList.count();
}由于该模型是非层次结构的,我们可以安全地忽略对应父项的模型索引。默认情况下,从QAbstractListModel 派生的模型仅包含一列,因此我们无需重写columnCount()函数。
模型头信息与数据
对于视图中的项目,我们希望返回字符串列表中的字符串。data() 函数负责返回与索引参数相对应的数据项:
QVariant StringListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= stringList.size())
return QVariant();
if (role == Qt::DisplayRole)
return stringList.at(index.row());
else
return QVariant();
}只有当提供的模型索引有效、行号在字符串列表中的项目范围内,且请求的角色是我们支持的角色时,才会返回有效的QVariant 。
某些视图(例如 `QTreeView ` 和 `QTableView`)能够同时显示标题和项目数据。如果我们的模型在带有标题的视图中显示,则希望标题中显示行号和列号。我们可以通过继承 `headerData()` 函数来提供有关标题的信息:
QVariant StringListModel::headerData(int section, Qt::Orientation orientation,
int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal)
return QStringLiteral("Column %1").arg(section);
else
return QStringLiteral("Row %1").arg(section);
}同样,只有当角色是我们支持的类型时,才会返回一个有效的 `QVariant `。在确定要返回的确切数据时,还会考虑标题的排列方向。
并非所有视图都会在标题中显示项目数据,而那些显示的视图也可能被配置为隐藏这些信息。尽管如此,仍建议您实现 `headerData()` 函数,以提供有关模型所提供数据的相关信息。
一个项可以具有多个角色,并根据指定的角色提供不同的数据。我们模型中的项仅有一个角色——DisplayRole ,因此无论指定了哪个角色,我们都会返回项的相关数据。不过,我们可以将为DisplayRole 提供的数据复用于其他角色,例如ToolTipRole ——视图可以利用该角色在工具提示中显示项的相关信息。
可编辑模型
只读模型展示了如何向用户呈现简单的选项,但对于许多应用程序而言,可编辑的列表模型要实用得多。 我们可以修改只读模型,使其中的项目可编辑——具体方法是修改我们为只读模式实现的 data() 函数,并额外实现两个函数:flags() 和setData()。在类定义中添加以下函数声明:
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;使模型可编辑
委托会在创建编辑器之前检查项目是否可编辑。模型必须让委托知道其项目是可编辑的。我们通过为模型中的每个项目返回正确的标志来实现这一点;在此情况下,我们启用所有项目,并使其既可选又可编辑:
Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled;
return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
}请注意,我们无需了解委托如何执行实际的编辑过程。我们只需提供一种方式,让委托能够设置模型中的数据。这通过setData()函数实现:
bool StringListModel::setData(const QModelIndex &index,
const QVariant &value, int role)
{
if (index.isValid() && role == Qt::EditRole) {
stringList.replace(index.row(), value.toString());
emit dataChanged(index, index, {role});
return true;
}
return false;
}在此模型中,字符串列表中与模型索引对应的项将被提供的值替换。不过,在修改字符串列表之前,我们必须确保索引有效、项的类型正确,且该角色受支持。 按惯例,我们要求角色必须是“EditRole ”,因为这是标准项委托所使用的角色。但对于布尔值,您可以使用“Qt::CheckStateRole ”并设置“Qt::ItemIsUserCheckable ”标志;此时将使用复选框来编辑该值。 该模型中所有角色的底层数据均相同,因此这一细节仅是为了便于将模型与标准组件集成。
数据设置完成后,模型必须通知视图某些数据已发生变化。这通过发出dataChanged() 信号来实现。由于仅有一项数据发生变化,因此信号中指定的项范围仅限于一个模型索引。
此外,还需要修改 data() 函数以添加对Qt::EditRole 的检测:
QVariant StringListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= stringList.size())
return QVariant();
if (role == Qt::DisplayRole || role == Qt::EditRole)
return stringList.at(index.row());
else
return QVariant();
}插入和删除行
可以更改模型中的行数和列数。在字符串列表模型中,只有更改行数才有意义,因此我们仅重写了插入和删除行的函数。这些函数在类定义中声明如下:
bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;由于该模型中的行对应于列表中的字符串,因此insertRows() 函数会在指定位置之前向字符串列表中插入若干个空字符串。插入的字符串数量等同于指定的行数。
父索引通常用于确定应在模型中的何处添加行。在此情况下,我们只有一个顶级字符串列表,因此只需将空字符串插入该列表中即可。
bool StringListModel::insertRows(int position, int rows, const QModelIndex &parent)
{
beginInsertRows(QModelIndex(), position, position+rows-1);
for (int row = 0; row < rows; ++row) {
stringList.insert(position, "");
}
endInsertRows();
return true;
}模型首先调用beginInsertRows()函数,以通知其他组件行数即将发生变化。 该函数指定了要插入的新行的首尾行号,以及其父项在模型中的索引。修改字符串列表后,它会调用endInsertRows()来完成操作,并通知其他组件模型的维度已发生变化,同时返回true表示操作成功。
从模型中删除行的函数编写起来也很简单。通过给定的位置和行数来指定要从模型中删除的行。为了简化实现,我们忽略父项索引,仅从字符串列表中删除相应的项。
bool StringListModel::removeRows(int position, int rows, const QModelIndex &parent)
{
beginRemoveRows(QModelIndex(), position, position+rows-1);
for (int row = 0; row < rows; ++row) {
stringList.removeAt(position);
}
endRemoveRows();
return true;
}在删除任何底层数据之前,总会调用beginRemoveRows()函数,该函数指定要删除的首行和末行。这使得其他组件能够在数据不可用之前访问这些数据。行被删除后,模型会触发endRemoveRows()来完成操作,并通知其他组件模型的维度已发生变化。
下一步
我们可以利用 `QListView ` 类将该模型(或任何其他模型)提供的数据以垂直列表的形式呈现。对于字符串列表模型,该视图还提供了一个默认编辑器,以便对项目进行操作。我们在“视图类”一节中探讨了标准视图类所提供的各种可能性。
《模型子类化参考》文档更详细地讨论了QAbstractItemModel 子类的要求,并提供了关于必须实现哪些虚函数以启用不同类型模型中各种功能的指南。
项目视图便捷类
基于项的小部件名称反映了其用途:QListWidget 提供一项列表,QTreeWidget 显示多级树结构,QTableWidget 提供单元格项表格。每个类都继承了QAbstractItemView 类的行为,该类实现了项选择和标题管理的通用行为。
列表控件
单级项目列表通常使用一个QListWidget 和多个QListWidgetItem来显示。列表控件的构建方式与其他控件相同:
QListWidget *listWidget = new QListWidget(this);在创建列表控件时,可以直接向其中添加列表项:
new QListWidgetItem(tr("Sycamore"), listWidget);
new QListWidgetItem(tr("Chestnut"), listWidget);
new QListWidgetItem(tr("Mahogany"), listWidget);也可以在不依赖父级列表控件的情况下单独创建列表项,并在稍后将其添加到列表中:
QListWidgetItem *newItem = new QListWidgetItem;
newItem->setText(itemText);
listWidget->insertItem(row, newItem);列表中的每个项目均可显示文本标签和图标。可通过更改文本渲染所用的颜色和字体,为项目提供自定义外观。工具提示、状态提示以及“这是什么?”帮助均可轻松配置,以确保列表与应用程序完美集成。
newItem->setToolTip(toolTipText);
newItem->setStatusTip(toolTipText);
newItem->setWhatsThis(whatsThisText);默认情况下,列表中的项目按创建顺序显示。可以根据Qt::SortOrder 中给出的标准对项目列表进行排序,从而生成按正向或反向字母顺序排序的项目列表:
树形控件
QTreeWidget 和QTreeWidgetItem 类提供了树形或分层列表。树形控件中的每个项目都可以拥有自己的子项目,并可显示多列信息。树形控件的创建方式与其他控件完全相同:
QTreeWidget *treeWidget = new QTreeWidget(this);在向树形控件中添加项目之前,必须先设置列数。例如,我们可以定义两列,并创建一个标题来为每列顶部提供标签:
treeWidget->setColumnCount(2);
QStringList headers;
headers << tr("Subject") << tr("Default");
treeWidget->setHeaderLabels(headers);设置各部分标签最简单的方法是提供一个字符串列表。若需更复杂的标题,可以构建一个树项,按需对其进行装饰,然后将其用作树控件的标题。
树控件中的顶级项目是以树控件本身作为父控件构建的。这些项目可以按任意顺序插入,或者您可以在构建每个项目时指定其前一个项目,以确保它们按特定顺序排列:
QTreeWidgetItem *cities = new QTreeWidgetItem(treeWidget);
cities->setText(0, tr("Cities"));
QTreeWidgetItem *osloItem = new QTreeWidgetItem(cities);
osloItem->setText(0, tr("Oslo"));
osloItem->setText(1, tr("Yes"));
QTreeWidgetItem *planets = new QTreeWidgetItem(treeWidget, cities);树控件处理顶级项的方式与处理树中较深层级的项略有不同。可以通过调用树控件的takeTopLevelItem()函数从树的顶级移除项,但要移除较低层级的项,则需调用其父项的takeChild()函数。 使用insertTopLevelItem()函数可在树的顶层插入项目。在树的较低层级,则需使用父节点的insertChild()函数。
在树的顶层和较低层级之间移动项目非常简单。我们只需检查项目是否为顶层项目,而这一信息由每个项目的parent() 函数提供。例如,我们可以移除树控件中的当前项目,无论其位置如何:
QTreeWidgetItem *parent = currentItem->parent();
int index;
if (parent) {
index = parent->indexOfChild(treeWidget->currentItem());
delete parent->takeChild(index);
} else {
index = treeWidget->indexOfTopLevelItem(treeWidget->currentItem());
delete treeWidget->takeTopLevelItem(index);
}将该项目插入树控件的其他位置也遵循相同的模式:
QTreeWidgetItem *parent = currentItem->parent();
QTreeWidgetItem *newItem;
if (parent)
newItem = new QTreeWidgetItem(parent, treeWidget->currentItem());
else
newItem = new QTreeWidgetItem(treeWidget, treeWidget->currentItem());表格控件
类似于电子表格应用程序中的项目表格,可通过QTableWidget 和QTableWidgetItem 构建。它们提供了一个带有表头和内部项目的可滚动表格控件。
表格可以预先设定行数和列数,也可以根据需要将项目添加到未设定大小的表格中。
QTableWidget *tableWidget;
tableWidget = new QTableWidget(12, 3, this);项目需在表外构建,然后添加到表中的指定位置:
QTableWidgetItem *newItem = new QTableWidgetItem(tr("%1").arg(
pow(row, column+1)));
tableWidget->setItem(row, column, newItem);可以通过在表格外部构建项目并将其用作标题,向表格中添加水平和垂直标题:
QTableWidgetItem *valuesHeaderItem = new QTableWidgetItem(tr("Values"));
tableWidget->setHorizontalHeaderItem(0, valuesHeaderItem);请注意,表格中的行和列从零开始计数。
常见特性
各便捷类中存在许多基于项的通用特性,这些特性可通过各类中相同的接口访问。我们将在后续章节中结合不同控件的示例介绍这些特性。有关各函数使用的更多详细信息,请参阅各控件的“模型/视图类”列表。
隐藏项目
有时,隐藏项视图控件中的项比直接删除更有用。上述所有控件中的项均可被隐藏,并可在稍后再次显示。您可以通过调用 isItemHidden() 函数来判断项是否被隐藏,并可通过setItemHidden() 隐藏项。
由于此操作基于项目,因此这三个便捷类均提供了相同的函数。
选择
项目的选择方式由控件的选择模式(QAbstractItemView::SelectionMode )决定。该属性控制用户可以选择单个还是多个项目,并在多项目选择时,控制所选项目是否必须为连续范围。上述所有控件的选择模式均按相同方式工作。
| 单项选择:当用户需要从小部件中选择单个项目时,默认的SingleSelection 模式最为合适。在此模式下,当前项目与选中项目相同。 |
| 多项选择:在此模式下,用户可以切换小部件中任意项目的选中状态,而不会改变现有选择,这与非互斥复选框可以独立切换的方式非常相似。 |
| 扩展选择:通常需要选择多个相邻项目的控件(例如电子表格中的控件)需要使用“ExtendedSelection ”模式。在此模式下,既可以通过鼠标也可以通过键盘选择控件中连续范围内的项目。 如果使用修饰键,还可以创建复杂选择,即包含许多与控件中其他已选项目不相邻的项目。如果用户未使用修饰键就选中了一个项目,则现有选择将清空。 |
小部件中选中的项目可通过selectedItems() 函数读取,该函数会提供一个可供遍历的相关项目列表。例如,我们可以使用以下代码计算选中项目列表中所有数值之和:
const QList<QTableWidgetItem *> selected = tableWidget->selectedItems();
int number = 0;
double total = 0;
for (QTableWidgetItem *item : selected) {
bool ok;
double value = item->text().toDouble(&ok);
if (ok && !item->text().isEmpty()) {
total += value;
number++;
}
}请注意,在单选模式下,当前项目将包含在选中范围内。而在多选和扩展选中模式下,根据用户建立选中范围的方式不同,当前项目可能不在选中范围内。
搜索
无论是作为开发者,还是作为向用户提供的一项服务,能够在项目视图控件中查找项目通常都非常有用。这三个项目视图便捷类都提供了通用的 `findItems() ` 函数,以使该操作尽可能一致且简单。
项目将根据Qt::MatchFlags 中选定的值所指定的条件,按其包含的文本进行搜索。我们可以使用findItems() 函数获取匹配项目的列表:
const QList<QTreeWidgetItem *> found = treeWidget->findItems(
itemText, Qt::MatchWildcard);
for (QTreeWidgetItem *item : found) {
item->setSelected(true);
// Show the item->text(0) for each item.
}上述代码会选中树控件中包含搜索字符串所指定文本的项目。此模式也可应用于列表和表格控件。
在项目视图中使用拖放功能
Qt 的拖放基础设施得到了模型/视图框架的全面支持。列表、表格和树中的项目可以在视图内拖动,数据可以作为 MIME 编码数据进行导入和导出。
标准视图自动支持内部拖放功能,即通过移动项目来更改其显示顺序。默认情况下,这些视图未启用拖放功能,因为它们的配置旨在满足最简单、最常见的使用场景。 若要允许拖动项目,需要启用视图的某些属性,且项目本身也必须支持拖动操作。
对于仅允许从视图中导出项目、但不允许将数据拖入其中的模型,其要求比完全启用拖放功能的模型要少。
有关在新模型中启用拖放支持的更多信息,请参阅《模型子类化参考》。
使用便捷视图
与 `QListWidget`、`QTableWidget` 和 `QTreeWidget ` 配合使用的每种项目类型,默认配置中都会使用一组不同的标志。例如,每个 `QListWidgetItem ` 或 `QTreeWidgetItem ` 初始状态下均处于启用状态,可选中、可选择,并可作为拖放操作的源;每个 `QTableWidgetItem ` 也可进行编辑,并可作为拖放操作的目标。
尽管所有标准项都已设置了一个或两个用于拖放的标志,但通常您需要设置视图本身的各种属性,才能充分利用其内置的拖放支持:
- 要启用项目拖拽功能,请将视图的dragEnabled 属性设置为
true。 - 若要允许用户在视图内放置内部或外部项目,请将视图的viewport()的acceptDrops 属性设置为
true。 - 若要向用户显示当前正在拖动的项目在释放后将被放置的位置,请设置视图的 `showDropIndicator ()` 方法的 ` ` 属性。这将向用户提供有关项目在视图中放置位置的实时更新信息。
例如,我们可以使用以下几行代码在列表控件中启用拖放功能:
QListWidget *listWidget = new QListWidget(this);
listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
listWidget->setDragEnabled(true);
listWidget->viewport()->setAcceptDrops(true);
listWidget->setDropIndicatorShown(true);结果是一个允许在视图内复制项目,甚至允许用户在包含相同类型数据的视图之间拖动项目的列表控件。在这两种情况下,项目都是被复制而非移动。
若要让用户能在视图内移动项目,我们必须设置列表控件的dragDropMode 属性:
listWidget->setDragDropMode(QAbstractItemView::InternalMove);使用模型/视图类
设置用于拖放的视图遵循与便捷视图相同的模式。例如,可以像设置QListWidget 一样设置QListView :
QListView *listView = new QListView(this);
listView->setSelectionMode(QAbstractItemView::ExtendedSelection);
listView->setDragEnabled(true);
listView->setAcceptDrops(true);
listView->setDropIndicatorShown(true);由于对视图所显示数据的访问由模型控制,因此所使用的模型也必须支持拖放操作。通过重写QAbstractItemModel::supportedDropActions()函数可以指定模型支持的操作。例如,可通过以下代码启用复制和移动操作:
Qt::DropActions DragDropListModel::supportedDropActions() const
{
return Qt::CopyAction | Qt::MoveAction;
}尽管可以指定Qt::DropActions 中任意值的组合,但模型需要被编写为支持这些操作。例如,为了使Qt::MoveAction 能够与列表模型正确配合使用,该模型必须提供QAbstractItemModel::removeRows()的实现,无论是直接实现还是通过继承自基类的实现。
为项目启用拖放功能
模型通过重写QAbstractItemModel::flags() 函数并提供相应的标志,向视图指示哪些项目可以被拖动,以及哪些项目可以接受拖放操作。
例如,一个基于QAbstractListModel 实现的简单列表模型,可以通过确保返回的标志包含Qt::ItemIsDragEnabled 和Qt::ItemIsDropEnabled 值,从而为每个项目启用拖放功能:
Qt::ItemFlags DragDropListModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QStringListModel::flags(index);
if (index.isValid())
return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
else
return Qt::ItemIsDropEnabled | defaultFlags;
}请注意,项目可以拖放到模型的顶级位置,但仅对有效项目启用拖拽功能。
在上述代码中,由于该模型继承自QStringListModel ,因此通过调用其实现的flags()函数,即可获得一组默认标志。
导出数据的编码
当数据项在拖放操作中从模型导出时,它们会被编码为与一种或多种 MIME 类型对应的适当格式。模型通过重写QAbstractItemModel::mimeTypes() 函数来声明可用于提供数据项的 MIME 类型,该函数返回一个标准 MIME 类型的列表。
例如,一个仅提供纯文本的模型将提供以下实现:
QStringList DragDropListModel::mimeTypes() const
{
QStringList types;
types << "application/vnd.text.list";
return types;
}该模型还必须提供将数据编码为声明格式的代码。这通过重写QAbstractItemModel::mimeData()函数来实现,该函数需返回一个QMimeData 对象,这与任何其他拖放操作相同。
以下代码展示了如何将对应于给定索引列表的每个数据项编码为纯文本,并存储在QMimeData 对象中。
QMimeData *DragDropListModel::mimeData(const QModelIndexList &indexes) const
{
QMimeData *mimeData = new QMimeData;
QByteArray encodedData;
QDataStream stream(&encodedData, QIODevice::WriteOnly);
for (const QModelIndex &index : indexes) {
if (index.isValid()) {
QString text = data(index, Qt::DisplayRole).toString();
stream << text;
}
}
mimeData->setData("application/vnd.text.list", encodedData);
return mimeData;
}由于向该函数提供了模型索引列表,因此这种方法具有足够的通用性,既适用于分层模型,也适用于非分层模型。
请注意,自定义数据类型必须声明为meta objects ,并且必须为其实现流运算符。详情请参阅QMetaObject 类的描述。
将已删除数据插入模型
任何给定模型处理已删除数据的方式,既取决于其类型(列表、表格或树),也取决于其内容向用户呈现的方式。通常,处理已删除数据的方法应最适合该模型底层的数据存储。
不同类型的模型通常以不同的方式处理被拖放的数据。 列表和表格模型仅提供一种扁平结构来存储数据项。因此,当数据被拖放到视图中的现有项目上时,它们可能会插入新行(和列),或者使用部分提供的数据覆盖模型中该项目的内容。 树模型通常能够向其底层数据存储中添加包含新数据的子项,因此从用户的角度来看,其行为会更加可预测。
拖放数据由模型对 `QAbstractItemModel::dropMimeData()` 的重写实现来处理。例如,一个处理简单字符串列表的模型可以提供一种实现,将拖放到现有项上的数据与拖放到模型顶层(即拖放到无效项上)的数据分别处理。
模型可以通过重写QAbstractItemModel::canDropMimeData()方法,禁止将数据拖放到某些项目上,或者根据被拖放的数据类型进行限制。
该模型首先必须确保该操作应被执行,提供的数据格式正确且可用,并且其在模型中的目标位置有效:
bool DragDropListModel::canDropMimeData(const QMimeData *data,
Qt::DropAction action, int row, int column, const QModelIndex &parent) const
{
Q_UNUSED(action);
Q_UNUSED(row);
Q_UNUSED(parent);
if (!data->hasFormat("application/vnd.text.list"))
return false;
if (column > 0)
return false;
return true;
}
bool DragDropListModel::dropMimeData(const QMimeData *data,
Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
if (!canDropMimeData(data, action, row, column, parent))
return false;
if (action == Qt::IgnoreAction)
return true;一个简单的单列字符串列表模型会在以下情况下提示失败:提供的数据不是纯文本,或者拖放时指定的列号无效。
要插入模型的数据,其处理方式取决于它是否被拖放到现有项目上。在这个简单示例中,我们希望允许将数据拖放到现有项目之间、列表中的第一个项目之前以及最后一个项目之后。
当发生拖放操作时,与父项目对应的模型索引要么是有效的(表明拖放操作发生在某个项目上),要么是无效的(表明拖放操作发生在视图中某个对应于模型顶级的位置)。
int beginRow;
if (row != -1)
beginRow = row;我们首先检查提供的行号,以确定是否可以将其用于将项目插入模型,无论父项索引是否有效。
else if (parent.isValid())
beginRow = parent.row();如果父模型索引有效,则拖放操作发生在某个项目上。在这个简单的列表模型中,我们会找出该项目的行号,并使用该值将拖放的项目插入到模型的顶层。
else
beginRow = rowCount(QModelIndex());当拖放发生在视图的其他位置,且行号无法使用时,我们将项目追加到模型的顶层。
在分层模型中,当某个项目被移除时,最好将新项目作为该项目的子项插入到模型中。在此示例中,模型仅有一层,因此这种方法并不适用。
解码导入的数据
dropMimeData() 的每个实现还必须对数据进行解码,并将其插入到模型的底层数据结构中。
对于简单的字符串列表模型,可以将编码后的项目解码并流式传输到 `QStringList` 中:
QByteArray encodedData = data->data("application/vnd.text.list");
QDataStream stream(&encodedData, QIODevice::ReadOnly);
QStringList newItems;
int rows = 0;
while (!stream.atEnd()) {
QString text;
stream >> text;
newItems << text;
++rows;
}随后,这些字符串可被插入到底层数据存储中。为保持一致性,可通过模型自身的接口完成此操作:
insertRows(beginRow, rows, QModelIndex());
for (const QString &text : std::as_const(newItems)) {
QModelIndex idx = index(beginRow, 0, QModelIndex());
setData(idx, text);
beginRow++;
}
return true;
}请注意,模型通常需要提供 `QAbstractItemModel::insertRows()` 和 `QAbstractItemModel::setData()` 函数的实现。
代理模型
在模型/视图框架中,由单个模型提供的数据项可被任意数量的视图共享,而每个视图可能以完全不同的方式呈现相同的信息。 自定义视图和委托是提供同一数据截然不同表示形式的有效方式。然而,应用程序通常需要针对同一数据的处理后版本提供常规视图,例如对项目列表进行不同排序的视图。
虽然将排序和过滤操作作为视图的内部功能来执行似乎很合适,但这种方法无法让多个视图共享此类可能耗时较长的操作结果。另一种方法是在模型内部进行排序,但这会导致类似的问题:每个视图都必须显示根据最近一次处理操作组织的数据项。
为了解决这个问题,模型/视图框架使用代理模型来管理各个模型与视图之间传递的信息。 代理模型是从视图角度来看行为与普通模型无异的组件,并代表该视图访问源模型中的数据。模型/视图框架所使用的信号和插槽确保了,无论视图与其源模型之间放置了多少个代理模型,每个视图都能得到恰当的更新。
使用代理模型
代理模型可以插入到现有模型与任意数量的视图之间。Qt 提供了一个标准代理模型 `QSortFilterProxyModel`,该模型通常直接实例化并使用,但也可以通过继承其子类来实现自定义的过滤和排序行为。QSortFilterProxyModel 类的使用方式如下:
QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(parent);
filterModel->setSourceModel(stringListModel);
QListView *filteredView = new QListView;
filteredView->setModel(filterModel);由于代理模型继承自QAbstractItemModel ,因此可以连接到任何类型的视图,并在视图之间共享。它们还可以用于以管道方式处理从其他代理模型获取的信息。
QSortFilterProxyModel 类的设计初衷是可在应用程序中直接实例化并使用。通过继承该类并实现所需的比较操作,可以创建更专业的代理模型。
自定义代理模型
通常,代理模型中使用的处理类型涉及将源模型中原始位置的每个数据项映射到代理模型中的另一个位置。在某些模型中,部分数据项可能在代理模型中没有对应的位置;此类模型即为过滤代理模型。 视图通过代理模型提供的模型索引访问项目,这些索引不包含任何关于源模型或源模型中原始项目位置的信息。
QSortFilterProxyModel 允许在将源模型的数据提供给视图之前对其进行过滤,还允许将源模型的内容作为预排序数据提供给视图。
自定义过滤模型
QSortFilterProxyModel 类提供了一个相当通用的过滤模型,可用于多种常见场景。对于高级用户,可以继承QSortFilterProxyModel 类,从而提供一种实现自定义过滤器的机制。
QSortFilterProxyModel 的子类可以重写两个虚函数,每当请求或使用代理模型中的模型索引时,这些函数就会被调用:
- filterAcceptsColumn() 用于从源模型的某一部分中过滤特定列。
- filterAcceptsRow() 用于从源模型的某一部分中筛选出特定的行。
QSortFilterProxyModel 中上述函数的默认实现会返回true,以确保所有项目都被传递给视图;若要过滤掉个别行和列,则应重写这些函数并返回false。
自定义排序模型
QSortFilterProxyModel 实例使用 std::stable_sort() 函数在源模型和代理模型的项之间建立映射关系,从而无需修改源模型的结构,即可向视图暴露已排序的项层次结构。要提供自定义排序行为,请重写lessThan() 函数以执行自定义比较。
模型子类化参考
模型子类需要为基类QAbstractItemModel 中定义的许多虚函数提供实现。需要实现的这些函数的数量取决于模型的类型——即它向视图提供的是简单列表、表格还是复杂的项层次结构。 继承自 `QAbstractListModel ` 和 `QAbstractTableModel ` 的模型可以利用这些类提供的函数默认实现。以树状结构展示数据项的模型必须为 `QAbstractItemModel` 中许多虚拟函数提供实现。
模型子类中需要实现的函数可分为三类:
- 项目数据处理:所有模型都需要实现相关函数,以便视图和委托能够查询模型的维度、检查项目并检索数据。
- 导航和索引创建:分层模型需要提供函数,以便视图可以调用这些函数来导航其暴露的树状结构,并获取项的模型索引。
- 拖放支持与MIME类型处理:模型继承了控制内部和外部拖放操作执行方式的函数。这些函数允许使用其他组件和应用程序能够理解的MIME类型来描述数据项。
项目数据处理
模型可以对其提供的数据提供不同级别的访问权限:它们可以是简单的只读组件,有些模型可能支持调整大小操作,还有些则允许编辑项目。
只读访问
要为模型提供的数据提供只读访问权限,必须在模型的子类中实现以下函数:
| flags() | 供其他组件用于获取模型提供的每个项的相关信息。在许多模型中,标志的组合应包括Qt::ItemIsEnabled 和Qt::ItemIsSelectable 。 |
| data() | 用于向视图和委托提供项目数据。通常,模型只需为Qt::DisplayRole 以及任何应用程序特定的用户角色提供数据,但为Qt::ToolTipRole 、Qt::AccessibleTextRole 和Qt::AccessibleDescriptionRole 提供数据也是良好的实践。有关每个角色关联的类型信息,请参阅Qt::ItemDataRole 枚举文档。 |
| headerData() | 为视图提供用于在标题中显示的信息。仅能显示标题信息的视图才会检索这些信息。 |
| rowCount() | 提供模型公开的数据行数。 |
所有类型的模型(包括列表模型(QAbstractListModel 的子类)和表格模型(QAbstractTableModel 的子类))都必须实现这四个函数。
此外,QAbstractTableModel 和QAbstractItemModel 的直接子类还必须实现以下函数:
| columnCount() | 提供模型公开的数据列数。列表模型不提供此函数,因为该函数已在QAbstractListModel 中实现。 |
可编辑项
可编辑模型允许修改数据项,并可能提供用于插入和删除行及列的函数。要启用编辑功能,必须正确实现以下函数:
| flags() | 必须为每个项目返回一组适当的标志组合。特别是,该函数返回的值除了包含只读模型中应用于项目的值外,还必须包含Qt::ItemIsEditable 。 |
| setData() | 用于修改与指定模型索引相关联的数据项。为了能够接受由用户界面元素提供的用户输入,该函数必须处理与Qt::EditRole 相关联的数据。该实现还可以接受与Qt::ItemDataRole 中指定的多种不同角色相关联的数据。在更改数据项后,模型必须发出dataChanged()信号,以通知其他组件该更改。 |
| setHeaderData() | 用于修改水平和垂直标题信息。修改数据项后,模型必须发出headerDataChanged() 信号,以通知其他组件发生了变化。 |
可调整大小的模型
所有类型的模型均可支持行插入和删除操作。 表格模型和分层模型还支持行级插入与删除。在模型维度发生变化之前和 之后,通知其他组件这些变化非常重要。因此,可以实现以下函数以允许调整模型大小,但实现时必须确保调用相应的函数来通知关联的视图和委托:
| insertRows() | 用于向所有类型的模型添加新行和数据项。实现必须在向任何底层数据结构插入新行之前调用beginInsertRows(),并在插入后立即调用endInsertRows()。 |
| removeRows() | 用于从所有类型的模型中删除行及其包含的数据项。实现必须在从任何底层数据结构中删除行之前调用beginRemoveRows(),并在删除后立即调用endRemoveRows()。 |
| insertColumns() | 用于向表模型和层次模型添加新列及其包含的数据项。实现必须在将新列插入任何底层数据结构之前调用beginInsertColumns(),并在之后立即调用endInsertColumns()。 |
| removeColumns() | 用于从表模型和层次模型中删除列及其所包含的数据项。实现必须在从任何底层数据结构中删除列之前调用beginRemoveColumns(),并在之后立即调用endRemoveColumns()。 |
通常,如果操作成功,这些函数应返回 true。但在某些情况下,操作可能仅部分成功;例如,插入的行数少于指定数量。在这种情况下,模型应返回 false 以指示失败,从而使任何关联组件能够处理该情况。
通过重定尺寸 API 实现中调用的函数发出的信号,使关联组件有机会在任何数据不可用之前采取行动。将插入和删除操作封装在 begin 和 end 函数中,也使模型能够正确管理persistent model indexes 。
通常,begin 和 end 函数能够将模型底层结构的变化通知给其他组件。对于模型结构的更复杂变化——例如涉及内部重组、数据排序或任何其他结构性变更——必须执行以下步骤:
- 发出 `layoutAboutToBeChanged()` 信号
- 更新代表模型结构的内部数据。
- 使用 `changePersistentIndexList()` 更新持久化索引
- 发出layoutChanged()信号。
此操作序列可用于任何结构性更新,以替代更高层次且更便捷的受保护方法。例如,若需从包含两百万行的模型中移除所有奇数行,这相当于移除 100 万个各含 1 个元素的非连续范围。 虽然可以调用 100 万次 beginRemoveRows 和 endRemoveRows,但这显然效率低下。相反,可以通过发送单一布局变更信号,一次性更新所有必要的持久化索引。
模型数据的延迟加载
模型数据的延迟加载实际上允许将模型信息的请求推迟到视图真正需要时才进行。
某些模型需要从远程源获取数据,或者必须执行耗时的操作才能获取有关数据组织方式的信息。由于视图通常会请求尽可能多的信息以准确显示模型数据,因此限制返回给视图的信息量有助于减少不必要的后续数据请求。
在分层模型中,若查询给定项的子项数量是一项开销较大的操作,则应确保仅在必要时才调用模型的rowCount()实现。 在这种情况下,可以重写 `hasChildren()` 函数,为视图提供一种低开销的方式来检查子项是否存在,并在 `QTreeView` 的情况下,为其父项绘制相应的装饰。
无论hasChildren()的重写版本是返回true 还是false ,视图可能都不需要调用rowCount()来查询子项的数量。例如,如果父项尚未展开以显示子项,则QTreeView 无需知道子项的数量。
如果已知许多项目会有子项,重新实现hasChildren() 方法并无条件返回true 有时是一种有用的做法。这既能确保后续可以检查每个项目的子项,又能使模型数据的初始加载尽可能快。 唯一的缺点是,在用户尝试查看不存在的子项之前,某些视图中没有子项的项目可能会显示不正确。
导航与模型索引的创建
层次模型需要提供函数,以便视图可以调用这些函数来导航其暴露的树状结构,并获取项的模型索引。
父项与子项
由于向视图暴露的结构由底层数据结构决定,因此每个模型子类都需要通过实现以下函数来创建自己的模型索引:
| index() | 给定父节点的模型索引,该函数允许视图和委托访问该节点的子节点。如果无法找到与指定的行、列和父模型索引相对应的有效子节点,则该函数必须返回 QModelIndex(),这表示一个无效的模型索引。 |
| parent() | 提供与任意给定子项的父项相对应的模型索引。如果指定的模型索引对应于模型中的顶级项,或者模型中不存在有效的父项,则该函数必须返回一个通过空的 QModelIndex() 构造函数创建的无效模型索引。 |
上述两个函数都使用createIndex() 工厂函数来生成供其他组件使用的索引。模型通常会向该函数提供某个唯一标识符,以确保模型索引日后能够与其对应的项重新关联。
拖放支持与 MIME 类型处理
模型/视图类支持拖放操作,并提供了足以满足许多应用程序需求的默认行为。不过,您也可以自定义拖放操作中项的编码方式、默认是复制还是移动,以及如何将项插入到现有模型中。
此外,便捷视图类实现了专门的行为,这些行为应与现有开发人员所期望的保持高度一致。“便捷视图”一节概述了这些行为。
MIME 数据
默认情况下,内置模型和视图使用内部 MIME 类型(application/x-qabstractitemmodeldatalist )来传递有关模型索引的信息。该类型指定了一组项目的数据,其中包含每个项目的行号和列号,以及每个项目所支持角色的信息。
可以通过调用QAbstractItemModel::mimeData() 并传入包含待序列化项的QModelIndexList 来获取使用此 MIME 类型编码的数据。
在自定义模型中实现拖放功能时,可以通过重新实现以下函数,将数据项导出为特定格式:
| mimeData() | 重写此函数可返回除默认application/x-qabstractitemmodeldatalist 内部MIME类型以外的其他格式的数据。子类可以从基类获取默认的QMimeData 对象,并以其他格式向其中添加数据。 |
对于许多模型而言,以text/plain 和image/png 等MIME类型所代表的通用格式提供项目内容非常有用。请注意,可以通过QMimeData::setImageData()、QMimeData::setColorData()和QMimeData::setHtml()函数轻松将图像、颜色和HTML文档添加到QMimeData 对象中。
接受拖放数据
当在视图上执行拖放操作时,系统会查询底层模型以确定其支持的操作类型以及可接受的 MIME 类型。这些信息由QAbstractItemModel::supportedDropActions() 和QAbstractItemModel::mimeTypes() 函数提供。未重写QAbstractItemModel 所提供实现的模型支持复制操作,并采用项目的默认内部 MIME 类型。
当序列化的项目数据被拖放到视图上时,数据会通过其QAbstractItemModel::dropMimeData()函数的实现插入到当前模型中。该函数的默认实现绝不会覆盖模型中的任何数据;相反,它会尝试将数据项插入为某个项目的同级项,或作为该项目的子项。
若要利用QAbstractItemModel 对内置MIME类型的默认实现,新模型必须重写以下函数:
| insertRows() | 这些函数使模型能够利用QAbstractItemModel::dropMimeData() 提供的现有实现自动插入新数据。 |
| insertColumns() | |
| setData() | 允许将项目填入新的行和列中。 |
| setItemData() | 此函数为填充新项目提供了更高效的支持。 |
若要支持其他形式的数据,必须重新实现以下函数:
| supportedDropActions() | 用于返回drop actions 的组合,以指示模型支持的拖放操作类型。 |
| mimeTypes() | 用于返回模型能够解码和处理的MIME类型列表。通常,模型支持作为输入的MIME类型与其在为外部组件编码数据时可使用的MIME类型相同。 |
| dropMimeData() | 执行对拖放操作传输的数据进行实际解码,确定在模型中的设置位置,并在必要时插入新的行和列。子类中该函数的具体实现取决于各模型所暴露的数据要求。 |
如果dropMimeData()函数的实现通过插入或移除行或列来改变模型的维度,或者数据项被修改,则必须谨慎确保所有相关信号均被触发。 为了确保模型行为的一致性,直接调用子类中其他函数的重写版本(例如setData()、insertRows() 和insertColumns())往往是可行的做法。
为了确保拖拽操作能够正常工作,必须重写以下用于从模型中移除数据的函数:
- removeRows()
- removeRow()
- removeColumns()
- removeColumn()
有关项目视图中拖放功能的更多信息,请参阅《在项目视图中使用拖放功能》。
便捷视图
便捷视图(QListWidget 、QTableWidget 和QTreeWidget )会覆盖默认的拖放功能,提供灵活性较低但更自然的行为,这适用于许多应用程序。 例如,由于将数据拖放到QTableWidget 的单元格中(用传输的数据替换现有内容)更为常见,因此底层模型会设置目标项的数据,而非在模型中插入新的行和列。有关便捷视图中拖放功能的更多信息,请参阅《在项目视图中使用拖放功能》。
海量数据的性能优化
canFetchMore() 函数会检查父项是否还有可用数据,并据此返回true 或false。fetchMore() 函数会根据指定的父项获取数据。这两个函数可以组合使用,例如在涉及增量数据的数据库查询中,用于填充QAbstractItemModel 。我们重写了canFetchMore() 函数以指示是否还有更多数据需要获取,并重写了fetchMore() 函数以按需填充模型。
另一个示例是动态填充的树形模型,当树形模型中的某个分支被展开时,我们会重写fetchMore() 方法。
如果您在重写fetchMore() 时向模型中添加了行,则需要调用beginInsertRows() 和endInsertRows()。此外,必须重写canFetchMore() 和fetchMore() 这两个方法,因为它们的默认实现返回 false 且不执行任何操作。
模型/视图类
这些类采用了模型/视图设计模式,其中底层数据(位于模型中)与用户对数据的呈现和操作方式(位于视图中)保持分离。
用于显示和编辑来自模型的数据项 | |
项目模型类的抽象接口 | |
项目视图类的基本功能 | |
可被子类化以创建一维列表模型的抽象模型 | |
代理项目模型的基类,可执行排序、过滤或其他数据处理任务 | |
可通过继承创建表格模型的抽象模型 | |
列视图的模型/视图实现 | |
代理多个源模型,并将它们的行进行拼接 | |
数据模型某部分与控件之间的映射 | |
用于本地文件系统的数据模型 | |
项目视图的标题行或标题列 | |
原样代理其源模型 | |
用于显示和编辑模型中数据项的功能 | |
无需继承 QItemEditorCreatorBase 即可创建项目编辑器创建器基类 | |
在实现新的项目编辑器创建器时必须继承的抽象基类 | |
用于在视图和委托中编辑项目数据的控件 | |
管理模型中选定项的相关信息 | |
跟踪视图中已选项目的 | |
管理模型中选定项目范围的相关信息 | |
模型的列表或图标视图 | |
基于项目的列表控件 | |
用于与 QListWidget 项目视图类配合使用的项目 | |
用于在数据模型中定位数据 | |
包含一个角色及其关联的数据 | |
覆盖 QModelRoleData 对象 | |
用于在数据模型中定位数据 | |
为任何 C++ 范围实现 QAbstractItemModel | |
该模板提供了一个自定义点,用于控制 QRangeModel 如何访问单个项的角色数据 | |
该模板提供了一个自定义点,用于控制 QRangeModel 如何表示用作行的类型 | |
以符合 QAbstractItemModel 规范的方式访问任何 C++ 范围 | |
提供一个用于遍历模型行中各列的 STL 风格非 const 迭代器 | |
提供一个用于遍历模型行中各列的 STL 风格非 const 迭代器 | |
提供一个用于遍历模型行且符合 STL 风格的 const 迭代器 | |
围绕 QRangeModel 中 const 行创建的引用封装器 | |
围绕 QRangeModel 中某项的引用包装器 | |
提供一个遍历模型行且遵循 STL 风格的非 const 迭代器 | |
围绕 QRangeModel 中一行创建的引用包装器 | |
RowReference 和 ConstRowReference 的通用 API | |
支持对在另一个模型与视图之间传递的数据进行排序和过滤 | |
用于 QStandardItemModel 类的项 | |
无需继承 QItemEditorCreatorBase 即可注册小部件 | |
用于存储自定义数据的通用模型 | |
向视图提供字符串的模型 | |
用于显示和编辑模型中数据项的功能 | |
表格视图的默认模型/视图实现 | |
基于项的表格视图及其默认模型 | |
用于 QTableWidget 类的项 | |
在不使用模型索引和选择模型的情况下与模型中的选择项进行交互的方法 | |
树视图的默认模型/视图实现 | |
使用预定义树模型的树视图 | |
用于 QTreeWidget 便利类的项 | |
遍历 QTreeWidget 实例中项的方法 |
相关示例
© 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.



