Qt 模型/视图编程
Model/View Programming
导论
Qt4 引入了一组新的项视图类,它们使用模型/视图体系结构来管理数据之间的关系以及数据呈现给用户的方式。该体系结构引入的功能分离,为开发人员提供了更大的灵活性来定制项的表示,并提供了一个标准模型接口,允许将大量数据源与现有项视图一起使用。在本文档中,我们简要介绍了模型/视图范例,概述了所涉及的概念,并描述了项视图系统的架构。本文解释了体系结构中的每个组件,并给出了如何使用提供的类的示例。
模型/视图的体系结构
模型-视图-控制器(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 是模型/视图框架中委托的抽象基类。从 Qt4.4 开始,默认的委托实现是由 QStyledItemDelegate 提供的,它被 Qt 的标准视图用作默认的委托。然而,QStyledItemDelegate 和QItemDelegate 是独立的替代方案,可以为视图中的项绘制和提供编辑器。它们之间的区别是,QStyledItemDelegate 使用当前样式来绘制它的项目。因此,我们建议在实现自定义委托或使用 Qt 样式表时使用QStyledItemDelegate 作为基类。
委托类一节中描述了委托。
排序
在模型/视图体系结构中有两种排序方法;选择哪种方法取决于您的底层模型。
如果你的模型是可排序的,也就是说,如果它重新实现了 QAbstractItemModel::sort() 函数,QTableView 和 QTreeView 都提供了一个API,允许你以编程的方式对模型数据进行排序。此外,你可以启用交互式排序(即允许用户通过点击视图的头排序数据),通过连接QHeaderView::sortIndicatorChanged() 信号分别到 QTableView::sortByColumn() 槽或 QTreeView::sortByColumn() 槽。
如果您的模型没有所需的接口,或者如果您想使用列表视图来表示数据,那么另一种方法是在视图中显示数据之前使用代理模型来转换模型的结构。这将在代理模型一节中详细介绍。
便利的类
许多便利的类是从标准视图类派生出来的,以便于依赖于 Qt 基于项的项视图和表类的应用程序。它们并不打算被子类化,而只是为 Qt3 中的等价类提供一个熟悉的接口。这些类的例子包括 QListWidget、QTreeWidget 和 QTableWidget;它们提供了与 Qt3 中的 QListBox、QListView 和 QTable 类类似的行为。
这些类不如视图类灵活,并且不能与任意模型一起使用。我们建议您使用模型/视图方法来处理项视图中的数据,除非您非常需要一组基于项的类。
如果您希望利用模型/视图方法提供的特性,同时仍然使用基于项的接口,请考虑使用视图类,如 QListView、QTableView 和 QTreeView 与 QStandardItemModel。
使用模型和视图
下面的章节将解释如何在 Qt 中使用模型/视图模式,每个章节包含一个示例,然后是如何创建新的组件。
Qt 中包含两个模型
Qt 提供的两个标准模型是 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 的所有子类都将数据表示为包含项表的层次结构。视图使用这种约定来访问模型中的数据项,但是它们在向用户显示信息的方式上没有限制。
模型还通过信号和插槽机制通知任何附加的视图数据的更改。
本节描述一些基本概念,这些概念与其他组件通过模型类访问数据项的方式密切相关。后面的部分将讨论更高级的概念。
模型索引
为了确保数据的表示与访问数据的方式保持分离,引入了模型索引的概念。通过模型可以获得的每一条信息都用模型索引表示。视图和委托使用这些索引来请求显示数据项。
因此,只有模型需要知道如何获取数据,并且模型管理的数据类型可以相当普遍地定义。模型索引包含一个指向创建它们的模型的指针,这在使用多个模型时防止了混淆。
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() 作为它们的父项目来引用。这将在下一节中讨论。
父项
当在表或列表视图中使用数据时,模型提供的用于项目数据的类表接口是理想的;行号和列号系统精确地映射到视图显示项的方式。然而,树形视图之类的结构要求模型向其中的项公开更灵活的接口。因此,每个项也可以是另一个项表的父项,就像树视图中的顶级项可以包含另一个项列表一样。
当请求模型项的索引时,我们必须提供一些关于该项父项的信息。在模型之外,唯一的方法是通过模型索引来引用一个项目,所以必须给出父模型索引:
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定义的。
我们可以通过向模型传递item对应的模型索引来获取item的数据,并通过指定一个角色来获取我们想要的数据类型:
QVariant value = model->data(index, role);
项角色
角色向模型指示所引用的数据类型。视图可以以不同的方式显示角色,因此为每个角色提供适当的信息很重要。
创建新模型部分更详细地介绍了角色的一些特定用途。
在 Qt::ItemDataRole 中定义的标准角色涵盖了项目数据最常见的用途。通过为每个角色提供适当的项目数据,模型可以为视图和委托提供关于项目应该如何呈现给用户的提示。不同类型的视图可以根据需要自由地解释或忽略这些信息。还可以为特定于应用程序的目的定义其他角色。
总结
- 模型索引以独立于任何底层数据结构的方式提供关于模型所提供项位置的视图和委托信息。
- 项通过它们的行号和列号以及它们的父项目的模型索引来引用。
- 模型索引是在视图和委托等其他组件的请求下由模型构造的。
- 如果在使用 index() 请求索引时为父项指定了一个有效的模型索引,则返回的索引将指向模型中父项目下面的一个项目。获得的索引指向该项的一个子项。
- 如果在使用 index() 请求索引时为父项指定了无效的模型索引,则返回的索引指向模型中的顶级项目。
- 角色区分与某项相关联的不同类型的数据。
使用模型索引
为了演示如何使用模型索引从模型中检索数据,我们设置了一个没有视图的 QFileSystemModel,并在小部件中显示文件和目录的名称。虽然这没有显示使用模型的正常方式,但它演示了模型在处理模型索引时使用的约定。
我们以以下方式构建文件系统模型:
QFileSystemModel *model = new QFileSystemModel;
QModelIndex parentIndex = model->index(QDir::currentPath());
int numRows = model->rowCount(parentIndex);
在本例中,我们设置了一个默认的 QFileSystemModel,使用该模型提供的 index() 的特定实现获取父索引,并使用 rowCount() 函数计算模型中的行数。
为简单起见,我们只对模型第一列中的项目感兴趣。我们依次检查每一行,获取每行中第一个项的模型索引,并读取模型中为该项存储的数据。
for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);
为了获得模型索引,我们指定行号、列号(第一列为0),以及我们想要的所有项目的父项目的适当模型索引。使用模型的 data() 函数检索存储在每个项中的文本。我们指定模型索引和 DisplayRole 来获取字符串形式的项目数据。
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
}
上面的例子演示了从模型中检索数据的基本原则:
- 可以使用rowCount()和columnCount()找到模型的维度。这些函数通常需要指定父模型索引。
- 模型索引用于访问模型中的项。需要行、列和父模型索引来指定项。
- 要访问模型中的顶级项目,请使用QModelIndex()指定一个空模型索引作为父索引。
- 项包含不同角色的数据。为了获得特定角色的数据,必须将模型索引和角色都提供给模型。
延伸阅读
通过实现 QAbstractItemModel 提供的标准接口,可以创建新的模型。在“创建新模型”一节中,我们通过创建一个保存字符串列表的方便易用的模型来演示这一点。
视图类
概念
在模型/视图体系结构中,视图从模型中获取数据项并将它们呈现给用户。数据的表示方式不必类似于模型提供的数据表示,而且可能与用于存储数据项的底层数据结构完全不同。
内容和表示的分离是通过使用 QAbstractItemModel 提供的标准模型接口、QAbstractItemView 提供的标准视图接口和模型索引来实现的,模型索引以一种通用的方式表示数据项。视图通常管理从模型中获得的数据的总体布局。它们可以呈现数据本身的单个项,或者使用委托同时处理呈现和编辑特性。
除了显示数据之外,视图还处理项之间的导航和项选择的某些方面。视图还实现了基本的用户界面功能,如上下文菜单和拖放。视图可以为项提供默认的编辑工具,也可以使用委托来提供自定义编辑器。
可以在没有模型的情况下构造视图,但是在显示有用信息之前必须提供模型。视图跟踪用户通过使用选项选择所选择的项,这些选项可以为每个视图单独维护,或者在多个视图之间共享。
一些视图,如 QTableView 和 QTreeView,显示标题和项。这些也是由视图类QHeaderView 实现的。头文件通常访问包含它们的视图的同一个模型。它们使用 QAbstractItemModel::headerData() 函数从模型中检索数据,通常以标签的形式显示头部信息。可以从 QHeaderView 类继承新的头文件,为视图提供更专门的标签。
使用现有视图
Qt 提供了三个可用的视图类,它们以大多数用户熟悉的方式显示来自模型的数据。QListView 可以将模型中的项目显示为一个简单的列表,或者以一个经典图标视图的形式显示。QTreeView 将模型中的项显示为列表的层次结构,允许以紧凑的方式表示深度嵌套的结构。QTableView 以表格的形式显示模型中的项目,很像电子表格应用程序的布局。
上面显示的