模型/视图编程
模型/视图编程简介
Qt 包含一组项目视图类,它们使用模型/视图架构来管理数据之间的关系以及向用户展示数据的方式。这种架构所引入的功能分离为开发人员提供了更大的灵活性来定制项目的显示方式,并提供了一个标准的模型接口,允许在现有的项目视图中使用各种数据源。在本文档中,我们将简要介绍模型/视图范式,概述相关概念,并描述项目视图系统的架构。我们将对体系结构中的每个组件进行解释,并举例说明如何使用所提供的类。
模型/视图体系结构
模型-视图-控制器(MVC)是一种源于 Smalltalk 的设计模式,通常用于构建用户界面。Gamma 等人在《设计模式》一书中写道:
MVC 包含三种对象。模型是应用程序对象,视图是其屏幕显示,而控制器则定义了用户界面对用户输入做出反应的方式。在 MVC 之前,用户界面设计往往将这些对象混为一谈。MVC 将它们分离开来,提高了灵活性和重用性。
如果将视图和控制器对象结合起来,结果就是模型/视图架构。这仍然是将数据的存储方式与向用户展示的方式分离开来,但基于相同的原则提供了一个更简单的框架。这种分离使得在不改变底层数据结构的情况下,在多个不同的视图中显示相同的数据和实现新类型的视图成为可能。为了灵活处理用户输入,我们引入了委托的概念。在这个框架中使用委托的好处是,它允许对数据项的呈现和编辑方式进行定制。
![]() | 模型/视图架构 模型与数据源通信,为架构中的其他组件提供接口。通信的性质取决于数据源的类型和模型的实现方式。 视图从模型中获取模型索引;这些索引是对数据项的引用。通过向模型提供模型索引,视图可以从数据源检索数据项。 在标准视图中,委托会渲染数据项。当编辑项目时,委托会使用模型索引直接与模型通信。 |
一般来说,模型/视图类可分为上述三组:模型、视图和委托。这些组件中的每一个都由抽象类定义,抽象类提供了通用接口,在某些情况下还提供了功能的默认实现。抽象类可以被子类化,以提供其他组件所期望的全套功能;这也允许编写专门的组件。
模型、视图和委托使用信号和槽相互通信:
- 来自模型的信号向视图通报数据源所持数据的变化。
- 视图发出的信号提供用户与显示项目交互的信息。
- 在编辑过程中,来自委托的信号用于向模型和视图通报编辑器的状态。
模型
所有项目模型都基于QAbstractItemModel 类。该类定义了视图和委托用于访问数据的接口。数据本身并不一定要存储在模型中;它可以存储在由单独的类、文件、数据库或其他应用程序组件提供的数据结构或存储库中。
有关模型的基本概念将在 "模型类"一节中介绍。
QAbstractItemModel 在模型类中,"视图 "为数据提供了一个接口,该接口足够灵活,可以处理以表格、列表和树的形式表示数据的视图。然而,在为列表和类似表格的数据结构实现新模型时, 和 类是更好的起点,因为它们提供了常用函数的适当默认实现。这些类都可以被子类化,以提供支持特殊类型列表和表格的模型。QAbstractListModel QAbstractTableModel
创建新模型一节将讨论子类化模型的过程。
Qt 提供了一些现成的模型,可用于处理数据项:
- QStringListModel 用于存储 项目的简单列表。QString
- QStandardItemModel 管理更复杂的项目树结构,每个项目可包含任意数据。
- QFileSystemModel 提供有关本地文件系统中文件和目录的信息。
- QSqlQueryModel QSqlTableModel 和 用于使用模型/视图约定访问数据库。QSqlRelationalTableModel
如果这些标准模型不能满足您的要求,您可以子类化QAbstractItemModel 、QAbstractListModel 或QAbstractTableModel ,创建自己的自定义模型。
视图
我们为不同类型的视图提供了完整的实现:QListView 显示项目列表,QTableView 在表格中显示模型数据,QTreeView 在分层列表中显示模型数据项。这些类都基于QAbstractItemView 抽象基类。虽然这些类都是即用型实现,但它们也可以被子类化,以提供自定义视图。
委托
QAbstractItemDelegate 是模型/视图框架中委托的抽象基类。默认委托实现由 提供,Qt 的标准视图将其用作默认委托。不过, 和 是为视图中的项目绘制和提供编辑器的独立替代方案。它们之间的区别在于, 使用当前样式来绘制项目。因此,我们建议在实现自定义委托或使用 Qt XML 样式表时使用 作为基类。QStyledItemDelegate QStyledItemDelegate QItemDelegate QStyledItemDelegate QStyledItemDelegate
排序
在模型/视图架构中有两种排序方法;选择哪种方法取决于您的底层模型。
如果您的模型是可排序的,即重新实现了QAbstractItemModel::sort() 函数,那么QTableView 和QTreeView 都提供了一个 API,允许您以编程方式对模型数据进行排序。此外,您还可以通过将QHeaderView::sortIndicatorChanged() 信号分别连接到QTableView::sortByColumn() 槽或QTreeView::sortByColumn() 槽,启用交互式排序(即允许用户通过点击视图的标题对数据进行排序)。
如果您的模型不具备所需的接口,或者您想使用列表视图来显示数据,另一种方法是在视图中显示数据之前,使用代理模型来转换模型的结构。代理模型一节将详细介绍这一点。
便利类
一些方便类是从标准视图类中派生出来的,它们有利于依赖 Qt 基于项目的项目视图和表格类的应用程序。我们并不打算对它们进行子类化。
这些类的例子包括QListWidget,QTreeWidget, 和QTableWidget 。
这些类不如视图类灵活,不能与任意模型一起使用。我们建议您使用模型/视图方法来处理项目视图中的数据,除非您非常需要一套基于项目的类。
如果你想利用模型/视图方法提供的功能,同时又想使用基于项的接口,可以考虑使用视图类,如QListView,QTableView, 和QTreeView 与QStandardItemModel 。
使用模型和视图
以下各节将介绍如何在 Qt 中使用模型/视图模式。每一节都包含一个示例,后面还有一节介绍如何创建新组件。
Qt 中包含的两种模型
Qt 提供的两个标准模型是QStandardItemModel 和QFileSystemModel 。QStandardItemModel 是一个多用途模型,可用于表示列表、表格和树视图所需的各种不同的数据结构。QFileSystemModel 是一个维护目录内容信息的模型。因此,它本身并不保存任何数据项,而只是表示本地文件系统中的文件和目录。
QFileSystemModel 我们可以使用《Acrobat Reader》提供的即用模型进行实验,并可以轻松配置以使用现有数据。利用这个模型,我们可以展示如何设置模型以便与现成的视图一起使用,并探索如何使用模型索引来操作数据。
使用现有模型的视图
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 XML 中,标准接口由QAbstractItemModel 类定义。无论数据项如何存储在任何底层数据结构中,QAbstractItemModel 的所有子类都将数据表示为包含项表的分层结构。视图使用这种约定来访问模型中的数据项,但它们向用户展示这些信息的方式不受限制。
模型也会通过信号和槽机制通知任何附加视图有关数据的更改。
本节将介绍一些基本概念,这些概念对于其他组件通过模型类访问数据项的方式至关重要。更高级的概念将在后面的章节中讨论。
模型索引
为了确保数据的表示与数据的访问方式分开,我们引入了模型索引的概念。可以通过模型获取的每条信息都由模型索引表示。视图和委托使用这些索引来请求显示数据项。
因此,只有模型需要知道如何获取数据,而且模型管理的数据类型可以定义得相当宽泛。模型索引包含一个指向创建它们的模型的指针,这可以防止在使用多个模型时出现混乱。
QAbstractItemModel *model = index.model();
模型索引提供了对信息块的临时引用,可用于通过模型检索或修改数据。由于模型可能会不时重组其内部结构,模型索引可能会失效,因此不应存储。如果需要对某条信息进行长期引用,就必须创建一个持久的模型索引。它提供了对模型保持最新的信息的引用。临时模型索引由QModelIndex 类提供,而持久模型索引则由QPersistentModelIndex 类提供。
要获得与数据项相对应的模型索引,必须为模型指定三个属性:行号、列号和父项的模型索引。下文将详细描述和解释这些属性。
行和列
在最基本的形式中,模型可以作为一个简单的表格来访问,在这个表格中,项是通过其行号和列号来定位的。这并不意味着底层数据存储在一个数组结构中;行和列编号的使用只是一种约定俗成的方式,允许组件之间相互通信。我们可以通过向模型指定行数和列数来检索任何给定项的信息,并收到一个代表该项的索引:
QModelIndex index = model->index(row, column, ...);
为简单的单层数据结构(如列表和表格)提供接口的模型不需要提供任何其他信息,但正如上述代码所示,我们在获取模型索引时需要提供更多信息。
![]() | 行和列 下图显示了一个基本表格模型,其中每个项目都由一对行号和列号定位。通过向模型传递相关的行和列编号,我们可以获得指向数据项的模型索引。 QModelIndex indexA = model->index(0, 0, QModelIndex()); QModelIndex indexB = model->index(1, 1, QModelIndex()); QModelIndex indexC = model->index(2, 1, QModelIndex()); 模型中的顶层项总是通过指定 |
项的父项
当使用表格或列表视图中的数据时,模型为项数据提供的类似表格的接口是最理想的;行和列编号系统与视图显示项的方式完全一致。但是,树形视图等结构要求模型为其中的项提供更灵活的接口。因此,每个项也可以是另一个项表的父项,就像树状视图中的顶层项可以包含另一个项列表一样。
在为模型项请求索引时,我们必须提供有关项父的一些信息。在模型外部,引用一个项的唯一方法是通过模型索引,因此也必须提供父模型索引:
QModelIndex index = model->index(row, column, parent);
![]() | 父项、行和列 下图显示了一个树状模型,其中每个项都由一个父项、一个行号和一个列号来表示。 项目 "A "和 "C "在模型中表示为顶层同级项目: QModelIndex indexA = model->index(0, 0, QModelIndex()); QModelIndex indexC = model->index(2, 1, QModelIndex()); 项目 "A "有多个子项。项目 "B "的模型索引可通过以下代码获得: QModelIndex indexB = model->index(1, 0, indexA); |
项目角色
模型中的项可以为其他组件执行各种角色,允许在不同情况下提供不同类型的数据。例如,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,在这个 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()
指定一个空模型索引作为父索引。 - 项包含不同角色的数据。要获取特定角色的数据,必须同时向模型提供模型索引和角色。
更多阅读
新模型可以通过实现QAbstractItemModel 提供的标准接口来创建。在 "创建新模型"部分,我们将通过创建一个用于保存字符串列表的方便的即用模型来演示这一点。
视图类
概念
在模型/视图架构中,视图从模型中获取数据项并将其呈现给用户。呈现数据的方式不必与模型提供的数据表示方式相似,也可能与用于存储数据项的底层数据结构完全不同。
通过使用QAbstractItemModel 提供的标准模型接口、QAbstractItemView 提供的标准视图接口,以及使用以一般方式表示数据项的模型索引,可以实现内容和显示的分离。视图通常管理从模型中获取的数据的整体布局。视图可以自己渲染单个数据项,也可以使用委托来处理渲染和编辑功能。
除了呈现数据外,视图还处理项目间的导航以及项目选择的某些方面。视图还能实现基本的用户界面功能,如上下文菜单和拖放。视图可以为项目提供默认的编辑功能,也可以与委托一起提供自定义编辑器。
视图可以在没有模型的情况下构建,但在显示有用信息之前必须提供模型。视图通过使用选择来跟踪用户选择的项目,选择可以为每个视图单独维护,也可以在多个视图之间共享。
有些视图,如QTableView 和QTreeView ,既显示标题,也显示项目。这些也是由视图类QHeaderView 实现的。标题通常与包含标题的视图访问相同的模型。它们使用QAbstractItemModel::headerData() 函数从模型中获取数据,并通常以标签的形式显示页眉信息。可以从QHeaderView 类子类化新的页眉,为视图提供更专业的标签。
使用现有视图
Qt XML 提供了三个现成可用的视图类,它们能以大多数用户熟悉的方式显示模型中的数据。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());
第二个视图将获得第一个视图的选择模型。现在,两个视图都在相同的选择模型上运行,从而使数据和所选项保持同步。
在上面的示例中,两个相同类型的视图用于显示同一模型的数据。但是,如果使用两种不同类型的视图,则每个视图中选定项的表示方法可能会截然不同;例如,表格视图中的连续选定项在树形视图中可能表示为一组高亮显示的零散项。
委托类
概念
与模型-视图-控制器模式不同,模型/视图设计不包括一个完全独立的组件来管理与用户的交互。一般来说,视图负责向用户展示模型数据和处理用户输入。为了使获取输入的方式具有一定的灵活性,交互是由委托来执行的。这些组件提供输入功能,还负责在某些视图中渲染单个项目。QAbstractItemDelegate 类中定义了控制委托的标准接口。
通过实现paint() 和sizeHint() 函数,委托应能自行呈现其内容。不过,基于简单部件的委托可以子类化QStyledItemDelegate 而不是QAbstractItemDelegate ,并利用这些函数的默认实现。
代表的编辑器可以通过使用部件管理编辑过程或直接处理事件来实现。本节稍后将介绍第一种方法。
使用现有委托
Qt XML 提供的标准视图使用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 确认他们在自旋框中的选择,委托就会将值提交到模型中并关闭自旋框。我们可以通过在自旋框上安装自己的事件过滤器来改变这种行为,并提供适合我们需要的编辑提示;例如,我们可以发出带有EditNextItem 提示的closeEditor() 来自动开始编辑视图中的下一个项目。
另一种无需使用事件过滤器的方法是提供我们自己的编辑器部件,为了方便起见,或许可以对QSpinBox 进行子类化。这种替代方法可以让我们以编写额外代码为代价,对编辑器部件的行为方式进行更多控制。如果需要自定义标准 Qt Widget 的行为,通常在委托中安装事件过滤器会更方便。
委托不一定要发出这些提示,但不发出这些提示的委托与应用程序的集成度较低,其可用性也不如那些发出提示以支持常用编辑操作的委托。
处理项目视图中的选择
概念
项目视图类中使用的选择模型提供了基于模型/视图架构设施的选择的一般描述。虽然用于处理选择的标准类对于所提供的项目视图来说已经足够,但选择模型允许您创建专门的选择模型,以满足您自己的项目模型和视图的要求。
视图中所选项目的相关信息存储在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 子类的要求在《模型子类化参考》文档中有更详细的描述。
设计模型
为现有数据结构创建新模型时,必须考虑应使用哪种类型的模型为数据提供接口。如果数据结构可以表示为项目列表或项目表,那么可以子类化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 ,因为这是标准 item 委托所使用的角色。但是,对于布尔值,您可以使用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 类以垂直列表的形式显示该模型或任何其他模型提供的数据。对于字符串列表模型,该视图还提供了一个默认编辑器,以便对项目进行操作。我们将在视图类中研究标准视图类提供的可能性。
模型子类化参考》(Model Subclassing Reference)文档更详细地讨论了QAbstractItemModel 子类的要求,并提供了必须实现的虚拟函数指南,以便在不同类型的模型中启用各种功能。
项目视图便捷类
基于项目的部件名称反映了它们的用途:QListWidget
提供项目列表,QTreeWidget
显示多级树结构,QTableWidget
提供单元格项目表。每个类都继承了QAbstractItemView
类的行为,该类实现了项目选择和标题管理的常用行为。
列表部件
单层项目列表通常使用QListWidget
和一些QListWidgetItem
s 来显示。列表部件的构造方法与其他部件相同:
QListWidget *listWidget = new QListWidget(this);
列表项可以在构建时直接添加到列表 widget 中:
new QListWidgetItem(tr("Sycamore"), listWidget); new QListWidgetItem(tr("Chestnut"), listWidget); new QListWidgetItem(tr("Mahogany"), listWidget);
它们也可以在没有父列表 widget 的情况下构建,并在以后添加到列表中:
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);
为每个部分设置标签的最简单方法是提供一个字符串列表。对于更复杂的标题,可以构建一个树状项,按自己的意愿进行装饰,然后将其用作树状 widget 的标题。
树形 widget 中的顶层项是以树形 widget 作为其父 widget 构建的。它们可以按任意顺序插入,也可以在构建每个项目时指定前一个项目,从而确保它们按特定顺序排列:
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()
函数都提供了这一信息。例如,我们可以删除树 widget 中的当前项目,而不管其位置如何:
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
构建的。它们提供了一个滚动表格 widget,其中包含标题和可使用的项目。
创建的表格可设定行数和列数,也可根据需要将这些行数和列数添加到未设定大小的表格中。
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);
请注意,表格中的行和列都从零开始。
常用功能
每个方便类都有一些基于项的共同特性,这些特性可通过每个类中的相同接口获得。我们将在下面的章节中介绍这些功能,并以不同的部件为例进行说明。请查看每个部件的模型/视图类列表,了解每个函数的使用详情。
隐藏项目
有时,在项目视图部件中隐藏项目比删除项目更有用。上述所有 widget 的项目都可以隐藏,然后再显示出来。您可以通过调用 isItemHidden() 函数来确定项目是否被隐藏,也可以通过setItemHidden()
来隐藏项目。
由于该操作是基于项目的,因此三个便利类都可以使用相同的函数。
选择
选择项目的方式由 widget 的选择模式 (QAbstractItemView::SelectionMode) 控制。该属性控制用户可以选择一个还是多个项目,在多项目选择中,选择是否必须是一个连续的项目范围。对于上述所有 widget,选择模式的作用都是一样的。
单个项目选择:当用户需要从一个 widget 中选择单个项目时,默认的SingleSelection 模式最为合适。在这种模式下,当前项目和所选项目是相同的。 | |
多项目选择:在这种模式下,用户可以在不改变现有选择的情况下切换 widget 中任何项目的选择状态,就像可以独立切换非排他性复选框一样。 | |
扩展选择:经常需要选择许多相邻项目的部件(如电子表格中的部件)需要使用ExtendedSelection 模式。在这种模式下,可以用鼠标和键盘选择部件中连续范围的项目。如果使用修改键,还可以创建复杂的选择,包括与 widget 中其他选定项目不相邻的许多项目。如果用户不使用修改键而选择了一个项目,则会清除现有的选择。 |
使用selectedItems()
函数可以读取 widget 中的选中项,并提供一个可以遍历的相关项目列表。例如,我们可以用以下代码求出所选项列表中所有数值的总和:
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++; } }
请注意,在单选模式下,当前项目将在选择范围内。在多选和扩展选择模式下,当前项目可能不在选择范围内,这取决于用户形成选择的方式。
搜索
无论是作为开发人员还是作为向用户提供的服务,在项目视图 widget 中查找项目通常都很有用。所有三个条目视图便利类都提供了一个通用的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. }
如果树状部件中的项目包含搜索字符串中给出的文本,则上述代码会使这些项目被选中。这种模式也可用于列表和表格 widget。
在项目视图中使用拖放功能
模型/视图框架完全支持 Qt 的拖放基础结构。列表、表格和树中的项目可在视图中拖动,数据可作为 MIME 编码的数据导入和导出。
标准视图自动支持内部拖放,通过移动项目来改变它们的显示顺序。默认情况下,这些视图不启用拖放功能,因为它们是为最简单、最常用的用途配置的。要允许拖放项目,需要启用视图的某些属性,而且项目本身也必须允许拖放。
与完全启用拖放功能的模型相比,只允许从视图中导出项目且不允许将数据投放到视图中的模型所需的条件较少。
有关在新模型中启用拖放支持的更多信息,请参阅《模型子类化参考》。
使用方便视图
与QListWidget 、QTableWidget 和QTreeWidget 一起使用的每种类型的项目默认都配置为使用一组不同的标志。例如,每个QListWidgetItem 或QTreeWidgetItem 最初都是启用的、可检查的、可选择的,并可用作拖放操作的来源;每个QTableWidgetItem 也可以编辑并用作拖放操作的目标。
虽然所有标准项目都为拖放设置了一个或两个标志,但通常需要在视图中设置各种属性才能利用内置的拖放支持:
- 要启用项目拖放,请将视图的dragEnabled 属性设置为
true
。 - 要允许用户在视图中拖放内部或外部项目,可将视图viewport() 的acceptDrops 属性设置为
true
。 - 若要向用户显示当前拖动的项目在下拉后会放置在何处,请设置视图的showDropIndicator 属性。这将为用户提供有关视图中项目放置位置的持续更新信息。
例如,我们可以通过以下几行代码在列表 widget 中启用拖放功能:
QListWidget *listWidget = new QListWidget(this); listWidget->setSelectionMode(QAbstractItemView::SingleSelection); listWidget->setDragEnabled(true); listWidget->viewport()->setAcceptDrops(true); listWidget->setDropIndicatorShown(true);
这样,列表 widget 就可以在视图中复制项目,甚至允许用户在包含相同类型数据的视图之间拖动项目。在这两种情况下,项目都是复制而不是移动。
为了让用户能在视图中移动项目,我们必须设置列表 widget 的dragDropMode :
listWidget->setDragDropMode(QAbstractItemView::InternalMove);
使用模型/视图类
为拖放设置视图与方便视图的设置模式相同。例如,设置QListView 的方法与设置QListWidget 的方法相同:
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 XML 提供了一个标准的代理模型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() 信号。
此序列可用于任何结构更新,以代替更高级、更方便的保护方法。例如,如果一个有 200 万行的模型需要移除所有奇数行,这就是 100 万个每行 1 个元素的连续范围。我们可以使用 beginRemoveRows 和 endRemoveRows 一百万次,但这显然效率不高。取而代之的是,可以通过一次布局更改来更新所有必要的持久化索引。
模型数据的懒迁移
模型数据的懒迁移可以有效地将对模型信息的请求推迟到视图实际需要时再处理。
有些模型需要从远程源获取数据,或者必须执行耗时的操作来获取有关数据组织方式的信息。由于视图通常会请求尽可能多的信息以准确显示模型数据,因此限制返回给视图的信息量以减少不必要的后续数据请求可能是有用的。
在分层模型中,查找给定项的子项数是一个昂贵的操作,因此确保只在必要时调用模型的rowCount() 实现是非常有用的。在这种情况下,可以重新实现hasChildren() 函数,为视图提供一种廉价的方法来检查是否存在子项,并在QTreeView 的情况下,为其父项绘制适当的装饰。
无论重新实现的hasChildren() 返回的是true
还是false
,视图可能都不需要调用rowCount() 来确定有多少个子项。例如,如果父项没有展开以显示子项,QTreeView 就不需要知道有多少个子项。
如果知道许多项目都会有子项目,那么重新实现hasChildren() 以无条件返回true
有时是一种有用的方法。这可以确保以后可以检查每个项是否有子项,同时使模型数据的初始化尽可能快。唯一的缺点是,没有子项的项目可能会在某些视图中显示错误,直到用户尝试查看不存在的子项。
导航和模型索引创建
分层模型需要提供视图可以调用的函数,以便导航它们所揭示的树状结构,并获取项的模型索引。
父项和子项
由于暴露给视图的结构是由底层数据结构决定的,因此每个模型子类都可以通过提供以下函数的实现来创建自己的模型索引:
index() | 给定父项的模型索引后,该函数允许视图和委托访问该项的子项。如果找不到与指定的行、列和父模型索引相对应的有效子项,该函数必须返回 QModelIndex(),这是一个无效的模型索引。 |
parent() | 提供与任何给定子项的父项相对应的模型索引。如果指定的模型索引与模型中的顶层项相对应,或者模型中没有有效的父项,函数必须返回一个无效的模型索引,该索引是用空的 QModelIndex() 构造函数创建的。 |
上述两个函数都使用createIndex() 工厂函数生成索引供其他组件使用。模型通常会向该函数提供一些唯一标识符,以确保模型索引以后能与相应的项目重新关联。
拖放支持和 MIME 类型处理
模型/视图类支持拖放操作,提供的默认行为足以满足许多应用程序的需要。不过,也可以自定义项目在拖放操作过程中的编码方式,默认情况下是否复制或移动项目,以及如何将项目插入现有模型。
此外,便捷视图类还实现了专门的行为,这些行为应与现有开发人员所期望的行为密切相关。方便视图"部分概述了这些行为。
MIME 数据
默认情况下,内置模型和视图使用内部 MIME 类型(application/x-qabstractitemmodeldatalist
)来传递有关模型索引的信息。这指定了一个项目列表的数据,其中包含每个项目的行和列编号,以及每个项目支持的角色信息。
使用这种 MIME 类型编码的数据可通过调用QAbstractItemModel::mimeData() 和包含要序列化的项目的QModelIndexList 来获取。
在自定义模型中实现拖放支持时,可以通过重新实现以下函数,以专门格式导出数据项:
mimeData() | 可以重新实现该函数,以便以默认application/x-qabstractitemmodeldatalist 内部 MIME 类型以外的格式返回数据。子类可以从基类中获取默认的QMimeData 对象,并在其中添加其他格式的数据。 |
对于许多模型来说,提供由 MIME 类型(如text/plain
和image/png
)表示的通用格式的项目内容是非常有用的。请注意,使用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 对象 | |
用于定位数据模型中的数据 | |
支持排序和过滤在另一个模型和视图之间传递的数据 | |
与 QStandardItemModel 类一起使用的项目 | |
无需子类化 QItemEditorCreatorBase 即可注册部件 | |
用于存储自定义数据的通用模型 | |
为视图提供字符串的模型 | |
为模型中的数据项提供显示和编辑功能 | |
表格视图的默认模型/视图实现 | |
带有默认模型的基于项的表格视图 | |
与 QTableWidget 类一起使用的项目 | |
在不使用模型索引和选择模型的情况下与模型中的选择进行交互的方法 | |
树形视图的默认模型/视图实现 | |
使用预定义树模型的树视图 | |
与 QTreeWidget 方便类一起使用的项目 | |
在 QTreeWidget 实例中遍历项目的方法 |
相关示例
© 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.