Qt 模型/视图编程

Qt 模型/视图编程

Model/View Programming


导论

Qt4 引入了一组新的项视图类,它们使用模型/视图体系结构来管理数据之间的关系以及数据呈现给用户的方式。该体系结构引入的功能分离,为开发人员提供了更大的灵活性来定制项的表示,并提供了一个标准模型接口,允许将大量数据源与现有项视图一起使用。在本文档中,我们简要介绍了模型/视图范例,概述了所涉及的概念,并描述了项视图系统的架构。本文解释了体系结构中的每个组件,并给出了如何使用提供的类的示例。

模型/视图的体系结构

模型-视图-控制器(MVC)是源自 Smalltalk 的一种设计模式,通常在构建用户界面时使用。在设计模式中,Gamma 等人写道:

MVC 由三种对象组成。模型是应用程序对象,视图是它的屏幕表示,控制器定义用户界面对用户输入作出反应的方式。在 MVC 出现之前,用户界面设计倾向于把这些对象放在一起。MVC 将它们解耦,以增加灵活性和重用性。

如果视图和控制器对象结合在一起,结果就是模型/视图体系结构。这仍然将数据的存储方式与数据呈现给用户的方式分开,但基于相同的原则提供了一个更简单的框架。这种分离使在几个不同的视图中显示相同的数据成为可能,并实现新的视图类型,而无需更改底层数据结构。为了灵活地处理用户输入,我们引入了委托的概念。在这个框架中使用委托的优点是,它允许自定义数据项的呈现和编辑方式

Qt 模型/视图编程_第1张图片

模型/视图的体系结构
模型与数据源通信,为体系结构中的其他组件提供接口。通信的性质取决于数据源的类型和模型的实现方式。
视图从模型中获取模型索引;这些是对数据项的引用。通过向模型提供模型索引,视图可以从数据源检索数据项。
在标准视图中,委托呈现数据项。当一个条目被编辑时,委托直接使用模型索引与模型通信。

通常,模型/视图类可以分为上面描述的三组:模型、视图和委托。这些组件都是由抽象类定义的,这些抽象类提供公共接口,在某些情况下,还提供特性的默认实现。抽象类的子类是为了提供其他组件所期望的完整功能集;这也允许编写专门的组件。

模型、视图和委托使用信号和插槽相互通信:

  • 来自模型的信号将数据源所持有的数据的更改通知视图。
  • 来自视图的信号提供了关于用户与被显示项的交互的信息。
  • 在编辑过程中使用来自委托的信号来告诉模型和视图编辑器的状态。

模型

所有的项模型都基于 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 一起使用的最合适的视图。下面的示例在树视图中显示一个目录的内容,它位于列表视图中相同信息的旁边。这些视图共享用户的选择,因此选中的项在两个视图中都突出显示。

Qt 模型/视图编程_第2张图片

我们设置了 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 的所有子类都将数据表示为包含项表的层次结构。视图使用这种约定来访问模型中的数据项,但是它们在向用户显示信息的方式上没有限制。

Qt 模型/视图编程_第3张图片

模型还通过信号和插槽机制通知任何附加的视图数据的更改。

本节描述一些基本概念,这些概念与其他组件通过模型类访问数据项的方式密切相关。后面的部分将讨论更高级的概念。

模型索引

为了确保数据的表示与访问数据的方式保持分离,引入了模型索引的概念。通过模型可以获得的每一条信息都用模型索引表示。视图和委托使用这些索引来请求显示数据项。

因此,只有模型需要知道如何获取数据,并且模型管理的数据类型可以相当普遍地定义。模型索引包含一个指向创建它们的模型的指针,这在使用多个模型时防止了混淆。

QAbstractItemModel *model = index.model();

模型索引提供了对信息片段的临时引用,并可用于通过模型检索或修改数据。由于模型可能会不时地重新组织其内部结构,因此模型索引可能会失效,不应该存储。如果需要对一段信息的长期引用,则必须创建一个持久的模型索引。这提供了对模型保持更新的信息的引用。临时模型索引由 QModelIndex 类提供,持久模型索引由 QPersistentModelIndex 类提供。

要获得与数据项相对应的模型索引,必须为模型指定三个属性:行号、列号和父项的模型索引。下面的部分将详细描述和解释这些属性。

行和列

在其最基本的形式中,可以将模型作为一个简单的表访问,其中的项目按其行号和列号进行定位。这并不意味着底层数据块存储在数组结构中;使用行号和列号只是允许组件彼此通信的一种约定。我们可以通过向模型指定任何给定项的行号和列号来检索有关该项的信息,并且我们会收到一个表示该项的索引:

QModelIndex index = model->index(row, column, ...);

为简单的、单级数据结构(如列表和表)提供接口的模型不需要提供任何其他信息,但正如上面的代码所指出的,在获取模型索引时,我们需要提供更多的信息。
Qt 模型/视图编程_第4张图片

行和列
该图显示了一个基本表模型的表示形式,其中每个项目都由一对行号和列号定位。通过将相关的行号和列号传递给模型,我们获得了一个引用数据项的模型索引。

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);

Qt 模型/视图编程_第5张图片

父、行和列
该图显示了一个树模型的表示形式,其中每个项目都由一个父类、一个行号和一个列号引用。
项目“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 模型/视图编程_第6张图片

项角色
角色向模型指示所引用的数据类型。视图可以以不同的方式显示角色,因此为每个角色提供适当的信息很重要。
创建新模型部分更详细地介绍了角色的一些特定用途。

在 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 以表格的形式显示模型中的项目,很像电子表格应用程序的布局。

Qt 模型/视图编程_第7张图片

上面显示的标准视图的默认行为对于大多数应用程序来说应该足够了。它们提供基本的编辑工具,并且可以进行定制以适应更专门的用户界面的需要。

使用一个模型

我们将创建的字符串列表模型作为示例模型,使用一些数据对其进行设置,并构造一个视图来显示模型的内容。这些都可以在一个函数中执行:

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 提供的列表视图足以显示字符串列表模型中的项目。我们构建视图,并使用以下代码行建立模型:

    QListView *view = new QListView;
    view->setModel(model);

视图按正常方式显示:

    view->show();
    return app.exec();
}

视图呈现模型的内容,通过模型的接口访问数据。当用户试图编辑项时,视图使用默认委托来提供编辑器小部件。

Qt 模型/视图编程_第8张图片

上图显示了 QListView 如何表示字符串列表模型中的数据。由于模型是可编辑的,视图自动允许使用默认委托编辑列表中的每个项目。

多个视图使用一个模型

为同一个模型提供多个视图仅仅是为每个视图设置相同的模型。在下面的代码中,我们创建了两个表视图,每个表视图都使用了我们为本例创建的相同的简单表模型:

    QTableView *firstTableView = new QTableView;
    QTableView *secondTableView = new QTableView;

    firstTableView->setModel(model);
    secondTableView->setModel(model);

在模型/视图体系结构中使用信号和槽意味着对模型的更改可以传播到所有附加的视图,确保我们总是可以访问相同的数据,而不管正在使用的视图是什么。

Qt 模型/视图编程_第9张图片

上面的图像显示了同一个模型的两个不同视图,每个视图都包含许多选定的项。尽管来自模型的数据在视图中一致地显示,但每个视图维护自己的内部选择模型。这在某些情况下是有用的,但是对于许多应用程序来说,共享选择模型是可取的。

处理项选择

处理视图中项选择的机制由QItemSelectionModel类提供。默认情况下,所有标准视图都构建自己的选择模型,并以正常的方式与它们交互。视图使用的选择模型可以通过 selectionModel() 函数获得,替换的选择模型可以通过 setSelectionModel() 指定。当我们希望为相同的模型数据提供多个一致的视图时,控制视图使用的选择模型的能力是有用的。

通常,除非您正在创建模型或视图的子类化,否则您不需要直接操作选择的内容。但是,如果需要,可以访问选择模型的接口,这将在处理项目视图中的选择中进行探讨。

在视图之间共享选择

虽然视图类在默认情况下提供它们自己的选择模型很方便,但当我们在同一个模型上使用多个视图时,通常希望在所有视图中一致地显示模型数据和用户的选择。由于视图类允许替换它们的内部选择模型,我们可以通过下面这行实现视图之间的统一选择:

secondTableView->setSelectionModel(firstTableView->selectionModel());

第二个视图给出了第一个视图的选择模型。现在,这两个视图都在同一个选择模型上操作,使数据和所选项保持同步。

Qt 模型/视图编程_第10张图片

在上面的示例中,使用了两个相同类型的视图来显示同一个模型的数据。然而,如果使用了两种不同类型的视图,则所选项目在每个视图中可能会以非常不同的方式表示;例如,表视图中的连续选择可以表示为树视图中高亮显示的项的片段集。

委托类

概念

与模型-视图-控制器模式不同,模型/视图设计不包含用于管理与用户交互的完全独立的组件。通常,视图负责向用户表示模型数据,并处理用户输入。为了允许获得输入的方式具有一定的灵活性,交互由委托执行。这些组件提供输入功能,还负责呈现某些视图中的单个项。控制委托的标准接口定义在 QAbstractItemDelegate 类中。

委托应该能够通过实现 paint() 和 sizeHint() 函数来呈现它们自己的内容。然而,简单的基于小部件的委托可以继承 QItemDelegate 而不是 QAbstractItemDelegate,并利用这些函数的默认实现。

可以通过使用小部件来管理编辑过程或直接处理事件来实现委托的编辑器。本节稍后将介绍第一种方法,它也在 Spin Box Delegate 示例中显示。

Pixelator 示例展示了如何创建一个自定义委托,该委托为表视图执行专门的呈现。

使用现有的委托

Qt 提供的标准视图使用 QItemDelegate 的实例来提供编辑工具。这个委托接口的默认实现为每个标准视图(QListView、QTableView和QTreeView)以通常的样式呈现项。

所有标准角色都由标准视图使用的默认委托处理。解释这些的方式在 QItemDelegate 文档中描述。

视图使用的委托由 itemDelegate() 函数返回。setItemDelegate() 函数允许你为一个标准视图安装一个自定义委托,当设置自定义视图的委托时需要使用这个函数。

一个简单的委托

这里实现的委托使用 QSpinBox 提供编辑工具,主要用于显示整数的模型。尽管我们为此目的设置了一个基于整数的自定义表模型,但是我们可以轻松地使用 QStandardItemModel,因为自定义委托控制数据输入。我们构造一个表视图来显示模型的内容,这将使用自定义委托进行编辑。

Qt 模型/视图编程_第11张图片

我们从 QItemDelegate 继承委托,因为我们不想编写自定义显示函数。但是,我们仍然必须提供管理编辑器小部件的函数:

class SpinBoxDelegate : public QItemDelegate
{
    Q_OBJECT

public:
    SpinBoxDelegate(QObject *parent = 0);

    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
                        const QModelIndex &index) const;

    void setEditorData(QWidget *editor, const QModelIndex &index) const;
    void setModelData(QWidget *editor, QAbstractItemModel *model,
                    const QModelIndex &index) const;

    void updateEditorGeometry(QWidget *editor,
        const QStyleOptionViewItem &option, const QModelIndex &index) const;
};

请注意,在构造委托时没有设置编辑器小部件。我们只在需要时构造编辑器小部件。

提供一个编辑

在本例中,当表视图需要提供一个编辑器时,它会要求委托提供一个适合被修改项的编辑器小部件。createEditor() 函数提供了委托能够设置合适的小部件所需的一切:

QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
    const QStyleOptionViewItem &/* option */,
    const QModelIndex &/* index */) const
{
    QSpinBox *editor = new QSpinBox(parent);
    editor->setMinimum(0);
    editor->setMaximum(100);

    return editor;
}

请注意,我们不需要保留指向编辑器小部件的指针,因为视图负责在不再需要它时销毁它。

我们在编辑器上安装委托的默认事件过滤器,以确保它提供用户期望的标准编辑快捷方式。可以将其他快捷方式添加到编辑器中,以允许更复杂的行为;这些将在编辑提示一节中讨论。

视图通过调用稍后为此目的定义的函数来确保正确设置编辑器的数据和几何图形。我们可以根据视图提供的模型索引创建不同的编辑器。例如,如果我们有一个整数列和一个字符串列,我们可以返回QSpinBox或QLineEdit,这取决于正在编辑的是哪个列。

委托必须提供一个函数将模型数据复制到编辑器中。在本例中,我们读取存储在 display 角色中的数据,并相应地在旋转框中设置该值。

void SpinBoxDelegate::setEditorData(QWidget *editor,
                                    const QModelIndex &index) const
{
    int value = index.model()->data(index, Qt::EditRole).toInt();

    QSpinBox *spinBox = static_cast(editor);
    spinBox->setValue(value);
}

在本例中,我们知道编辑器小部件是一个旋转框,但是我们可以为模型中不同类型的数据提供不同的编辑器,在这种情况下,我们需要在访问其成员函数之前将小部件强制转换为适当的类型。

向模型提交数据

当用户完成编辑旋转框中的值时,视图通过调用 setModelData() 函数要求委托将编辑后的值存储在模型中。

void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
                                const QModelIndex &index) const
{
    QSpinBox *spinBox = static_cast(editor);
    spinBox->interpretText();
    int value = spinBox->value();

    model->setData(index, value, Qt::EditRole);
}

因为视图管理委托的编辑器小部件,所以我们只需要用所提供的编辑器的内容更新模型。在本例中,我们确保旋转框是最新的,并使用指定的索引用它包含的值更新模型。

标准的 QItemDelegate 类在视图完成编辑时发出 closeEditor() 信号来通知视图。视图确保编辑器小部件被关闭和销毁。在这个例子中,我们只提供了简单的编辑工具,所以我们永远不需要发出这个信号。

对数据的所有操作都是通过 QAbstractItemModel 提供的接口执行的。这使得委托在很大程度上独立于它所操作的数据类型,但是为了使用某些类型的编辑器小部件,必须做出一些假设。在这个例子中,我们假设模型总是包含整数值,但是我们仍然可以在不同类型的模型中使用这个委托,因为 QVariant 为意外数据提供了合理的默认值。

更新编辑器的几何图形

委托的职责是管理编辑器的几何图形。当编辑器创建时,以及项目的大小或在视图中的位置改变时,必须设置几何图形。幸运的是,视图在视图选项对象中提供了所有必要的几何信息。

void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
    const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
    editor->setGeometry(option.rect);
}

在本例中,我们只使用项目矩形中的视图选项提供的几何信息。呈现带有多个元素的项的委托不会直接使用项矩形。它将根据项目中的其他元素来定位编辑器。

编辑提示

编辑之后,委托应该向其他组件提供有关编辑过程结果的提示,并提供有助于任何后续编辑操作的提示。这是通过发送带有适当提示的 closeEditor() 信号来实现的。这由默认的QItemDelegate 事件筛选器负责,该事件筛选器是我们在构建旋转框时安装的。

旋转盒的行为可以调整,使其更友好的用户。在 QItemDelegate 提供的默认事件筛选器中,如果用户点击 Return 以在旋转框中确认他们的选择,委托将值提交给模型并关闭旋转框。我们可以通过在旋转框上安装我们自己的事件过滤器来改变这种行为,并提供适合我们需要的编辑提示;例如,我们可以发出带有 EditNextItem 提示的 closeEditor() 来自动开始编辑视图中的下一个项目。

另一种不需要使用事件筛选器的方法是提供我们自己的编辑器小部件,也许为了方便可以子类化 QSpinBox。这种替代方法将让我们以编写额外代码为代价,对编辑器小部件的行为进行更多控制。如果需要自定义标准Qt编辑器小部件的行为,通常在委托中安装事件过滤器会更容易。

委托不必发出这些提示,但是不发出这些提示的委托将较少地集成到应用程序中,并且与发出提示以支持常见编辑操作的委托相比,它们的可用性较差。

处理项视图中的选择

概念

与 Qt3 中使用的选择模型相比,项视图类中使用的选择模型提供了许多改进。它基于模型/视图体系结构的功能提供了更通用的选择描述。尽管用于操作选择的标准类对于提供的项视图来说已经足够了,但是选择模型允许您创建专门的选择模型来满足您自己的项模型和视图的需求。

关于视图中所选项的信息存储在 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);

使用由选择标志组合定义的命令将选择应用到选择模型。在这种情况下,所使用的标志导致选择对象中记录的项包含在选择模型中,而不管它们以前的状态如何。结果选择由视图显示。

Qt 模型/视图编程_第12张图片

项目的选择可以使用由选择标志定义的各种操作来修改。这些操作产生的选择可能具有复杂的结构,但选择模型可以有效地表示它。当我们研究如何更新选择时,将描述如何使用不同的选择标志来操作所选项。

读取选择状态

可以使用 selectedinindexes() 函数读取存储在选择模型中的模型索引。这将返回一个无序的模型索引列表,我们可以遍历这些索引,只要我们知道它们是针对哪个模型的:

QModelIndexList indexes = selectionModel->selectedIndexes();
QModelIndex index;

foreach(index, indexes) {
    QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
    model->setData(index, text);
}

上面的代码使用 Qt 方便的 foreach 关键字来迭代和修改选择模型返回的索引所对应的项。

选择模型发出信号来指示选择中的变化。这些通知其他组件对整个选择和项模型中当前聚焦项的更改。我们可以将 selectionChanged() 信号连接到插槽,并在选择更改时检查模型中被选中或取消选中的项。槽被两个 QItemSelection 对象调用:一个包含与新选择项对应的索引列表;另一个包含与新取消选择的项对应的索引。

在下面的代码中,我们提供了一个接收 selectionChanged() 信号的槽,用字符串填充选中的项,并清除未选中项的内容。

void MainWindow::updateSelection(const QItemSelection &selected,
    const QItemSelection &deselected)
{
    QModelIndex index;
    QModelIndexList items = selected.indexes();

    foreach (index, items) {
        QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
        model->setData(index, text);
    }

    items = deselected.indexes();

    foreach (index, items)
        model->setData(index, "");
}

我们可以通过将 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 标志使选择模型颠倒指定项的状态,选择给定的任何取消选择的项,并取消当前选择的任何项。“取消选择”标志将取消选择所有指定项目。

通过创建项的选择,并将它们应用到选择模型,可以更新选择模型中的单个项。在下面的代码中,我们对上面显示的表模型应用第二次项目选择,使用 Toggle 命令反转给定项目的选择状态。

QItemSelection toggleSelection;

topLeft = model->index(2, 1, QModelIndex());
bottomRight = model->index(7, 3, QModelIndex());
toggleSelection.select(topLeft, bottomRight);

selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);

这个操作的结果显示在表格视图中,提供了一种方便的方式来可视化我们所取得的成果:

Qt 模型/视图编程_第13张图片

默认情况下,选择命令只对模型索引指定的单个项进行操作。但是,用于描述选择命令的标志可以与其他标志结合使用,以更改整个行和列。例如,如果您仅使用一个索引调用 select(),但使用一个 select 和 Rows 组合的命令,则会选择包含引用项的整个行。下面的代码演示了行和列标志的使用:

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);

虽然只向选择模型提供了四个索引,但是使用列和行选择标志意味着选择了两列和两行。下图显示了这两个选择的结果:

Qt 模型/视图编程_第14张图片

在示例模型上执行的命令都涉及到在模型中积累项目的选择。也可以清除所选内容,或者用新内容替换当前内容。
若要用新选择替换当前选择,请将其他选择标志与 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 类提供了对更简单的非层次数据结构的接口的支持,并且更容易用作简单列表和表模型的起点。

在本节中,我们将创建一个简单的只读模型来探索模型/视图体系结构的基本原则。在本节稍后的部分中,我们将修改这个简单的模型,以便用户可以修改项。

有关更复杂模型的示例,请参见Simple Tree Model(简单树模型)示例。

QAbstractItemModel 子类的需求在 Model Subclassing Reference 文档中有更详细的描述。

设计一个模型

在为现有数据结构创建新模型时,重要的是要考虑应该使用哪种类型的模型来为数据提供接口。如果数据结构可以表示为列表或项目表,则可以继承 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 = 0)
        : QAbstractListModel(parent), stringList(strings) {}

    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const;

private:
    QStringList stringList;
};

除了模型的构造函数外,我们只需要实现两个函数:rowCount() 返回模型中的行数,data() 返回与指定的模型索引对应的数据项。

行为良好的模型还实现了 headerData() 来为树和表视图提供一些在它们的头文件中显示的内容。

注意,这是一个非层次模型,因此我们不必担心父-子关系。如果我们的模型是分层的,我们还必须实现 index() 和 parent() 函数。

字符串列表内部存储在 stringList 私有成员变量中。

模型尺寸

我们希望模型中的行数与字符串列表中的字符串数相同。我们在实现 rowCount() 函数时要记住以下几点:

int StringListModel::rowCount(const QModelIndex &parent) const
{
    return stringList.count();
}

由于模型是非层次结构的,我们可以安全地忽略与父项对应的模型索引。默认情况下,从 QAbstractListModel 派生的模型只包含一列,因此我们不需要重新实现 columnCount() 函数。

模型头和数据

对于视图中的项目,我们希望返回字符串列表中的字符串。data() 函数负责返回与 index 参数对应的数据项:

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 QString("Column %1").arg(section);
    else
        return QString("Row %1").arg(section);
}

同样,只有当角色是我们支持的角色时,我们才返回一个有效的 QVariant。在决定要返回的确切数据时,也要考虑头部的方向。

并不是所有的视图都显示带有项目数据的标题,而那些这样做的视图可能被配置为隐藏它们。尽管如此,还是建议您实现 headerData() 函数来提供关于模型提供的数据的相关信息。

一个项可以有多个角色,根据指定的角色给出不同的数据。模型中的项只有一个角色 DisplayRole,因此我们返回项的数据,而不管指定的角色是什么。但是,我们可以在其他角色中重用我们为 DisplayRole 提供的数据,比如视图可以使用 ToolTipRole 在工具提示中显示有关项目的信息。

一个可编辑的模型

只读模型向用户展示了如何简单地选择,但对于许多应用程序来说,可编辑列表模型更有用。我们可以通过修改为只读实现的 data() 函数,以及实现两个额外的函数:flags() 和 setData(),来修改只读模型,使条目可编辑。以下函数声明被添加到类定义中:

Qt::ItemFlags flags(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value,
            int role = Qt::EditRole);

使模型可编辑

委托在创建编辑器之前检查项是否可编辑。模型必须让委托知道它的项是可编辑的。为此,我们为模型中的每个项目返回正确的标志;在本例中,我们启用所有项,并使它们既可选择又可编辑:

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);
        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());
bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex());

由于这个模型中的行对应于列表中的字符串,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 子类的需求,并提供了虚函数的指南,这些虚函数必须被实现来支持不同类型的模型中的各种特性。

项视图便利类

Qt4 还引入了一些标准的小部件来提供经典的基于项的容器小部件。它们的行为类似于 Qt3 中的项视图类,但是为了提高性能和可维护性,已经重写了使用底层模型/视图框架。旧的项视图类在兼容库中仍然可用(更多信息请参见移植指南)。

基于项的小部件被赋予了反映其用途的名称: 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);

列表中的每一项都可以显示一个文本标签和一个图标。可以更改用于呈现文本的颜色和字体,以为项目提供定制的外观。工具提示、状态提示和“What’s This?”帮助都很容易配置,以确保列表被正确地集成到应用程序中。

newItem->setToolTip(toolTipText);
newItem->setStatusTip(toolTipText);
newItem->setWhatsThis(whatsThisText);

默认情况下,列表中的项是按照它们的创建顺序显示的。项目列表可以根据 Qt::SortOrder 中给出的标准进行排序,以产生一个按正向或反向字母顺序排序的项目列表:

listWidget->sortItems(Qt::AscendingOrder);
listWidget->sortItems(Qt::DescendingOrder);

树小部件

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);

注意,表中的行和列从0开始。

共同的特征

每个便利类都有许多基于项的通用特性,这些特性可以通过每个类中的相同接口使用。在下面的小节中,我们将通过一些不同小部件的示例来介绍这些小部件。查看每个小部件的模型/视图类列表,了解使用的每个函数的更多细节。

隐藏的项

有时,能够在项视图小部件中隐藏项而不是删除它们是很有用的。上面所有小部件的项都可以隐藏,然后再显示。您可以通过调用 isItemHidden() 函数来确定一个项是否隐藏,并且可以使用 setItemHidden() 来隐藏项。

因为这个操作是基于项的,所以这三个便利类都可以使用相同的函数。

选择

选择项的方式是由小部件的选择模式(QAbstractItemView::SelectionMode)控制的。此属性控制用户是可以选择一个项目还是多个项目,以及在多个项目选择中,选择是否必须是一个连续的项目范围。对于上述所有小部件,选择模式的工作方式是相同的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kBanp36s-1620900187193)(selection-single.png)]

单项选择:当用户需要从小部件中选择单项时,默认的SingleSelection模式是最合适的。在这种模式下,当前项和所选项是相同的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNUOrmpm-1620900187193)(selection-multi.png)]

多项选择:在这种模式下,用户可以切换小部件中任何项的选择状态,而不改变现有的选择,这与非排他的复选框可以独立切换的方式非常相似。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rES20WV7-1620900187194)(selection-extended.png)]

扩展选择:通常需要选择许多相邻项的小部件(如电子表格中的部件)需要 ExtendedSelection 模式。在这种模式下,可以用鼠标和键盘选择小部件中连续范围的项目。如果使用修饰符键,还可以创建复杂的选择,其中包括与小部件中其他选定项不相邻的许多项。
如果用户选择了一个没有使用修饰符键的项目,现有的选择将被清除。

使用 selectedItems() 函数读取小部件中选中的项,该函数提供了可以迭代的相关项列表。例如,我们可以用下面的代码找到一个选中项列表中所有数值的和:

QList selected = tableWidget->selectedItems();
    QTableWidgetItem *item;
    int number = 0;
    double total = 0;

    foreach (item, selected) {
        bool ok;
        double value = item->text().toDouble(&ok);

        if (ok && !item->text().isEmpty()) {
            total += value;
            number++;
        }
    }

注意,对于单一选择模式,当前项将位于选择中。在多选和扩展选择模式中,当前项目可能不在选择范围内,这取决于用户形成选择的方式。

搜索

无论是作为开发人员还是作为向用户展示的服务,能够在项目视图小部件中找到项目通常是很有用的。这三个项目视图便利类都提供了一个公共的 findItems() 函数,以尽可能保持一致和简单。

根据从 Qt::MatchFlags 中选择的值指定的条件,由它们所包含的文本搜索项。我们可以通过 findItems() 函数获得匹配项的列表:

QTreeWidgetItem *item;
QList found = treeWidget->findItems(
    itemText, Qt::MatchWildcard);

foreach (item, found) {
    treeWidget->setItemSelected(item, true);
    // Show the item->text(0) for each item.
}

如果树形小部件中的项包含搜索字符串中给出的文本,那么上面的代码将导致它们被选中。此模式也可用于列表和表小部件。

对项视图使用拖放

模型/视图框架完全支持 Qt 的拖放基础结构。列表、表和树中的项可以在视图中拖动,数据可以作为 MIME 编码的数据导入和导出。

标准视图自动支持内部拖放,项目可以移动,以改变它们显示的顺序。默认情况下,没有为这些视图启用拖放,因为它们是为最简单、最常见的用途配置的。要允许拖动项,需要启用视图的某些属性,并且项本身也必须允许拖动。

对于一个只允许从视图中导出项,并且不允许将数据放入其中的模型,其需求要比一个完全启用的拖放模型的需求要少。

有关在新模型中启用拖放支持的更多信息,请参见模型子类化参考(Model Subclassing Reference)。

使用方便的视图

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);

使用模型/视图类

设置用于拖放的视图遵循与便利视图相同的模式。例如, 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);

    foreach (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;
}

由于向函数提供了一组模型索引,因此这种方法足够通用,可以用于分层模型和非分层模型。

注意,自定义数据类型必须声明为元对象,并且必须为它们实现流操作符。详见 QMetaObject 类描述。

将删除的数据插入模型中

任何给定的模型处理删除数据的方式取决于它的类型(列表、表或树)和它的内容可能呈现给用户的方式。通常,用于容纳删除数据的方法应该是最适合模型底层数据存储的方法。

不同类型的模型倾向于以不同的方式处理丢失的数据。列表和表模型只提供存储数据项的平面结构。因此,当数据被拖放到视图中的现有项上时,它们可能会插入新的行(和列),或者它们可能会使用提供的一些数据覆盖模型中该项的内容。树模型通常能够将包含新数据的子项添加到其底层数据存储中,因此对于用户而言,它的行为更加可预测。

被删除的数据由模型的 QAbstractItemModel::dropMimeData() 的重新实现来处理。例如,一个处理简单字符串列表的模型可以提供一个实现,该实现将放在现有项上的数据单独处理到模型顶层的数据(即放在一个无效项上)。

模型首先必须确保操作应该被执行,提供的数据的格式是可以使用的,并且它在模型中的目的地是有效的:

bool DragDropListModel::dropMimeData(const QMimeData *data,
    Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    if (action == Qt::IgnoreAction)
        return true;

    if (!data->hasFormat("application/vnd.text.list"))
        return false;

    if (column > 0)
        return false;

如果提供的数据不是纯文本,或者为删除提供的列号无效,一个简单的单列字符串列表模型可以指示失败。

根据是否将插入模型的数据放到现有项上,将对数据进行不同的处理。在这个简单的示例中,我们希望允许在现有项之间、列表中第一项之前和最后一项之后进行删除。

当一个删除发生时,对应于父项目的模型索引要么是有效的,表明删除发生在一个项目上,要么是无效的,表明删除发生在与模型顶层相对应的视图的某个地方。

    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());
    foreach (const QString &text, 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 实例使用 Qt 的内置 qStableSort() 函数来建立源模型和代理模型中的项之间的映射,允许在不修改源模型结构的情况下将项的排序层次结构暴露给视图。要提供自定义排序行为,请重新实现 lessThan() 函数来执行自定义比较。

自定义数据模型

QIdentityProxyModel 实例并不对源模型的结构进行排序或筛选,而是提供一个用于创建数据代理的基类。这在 QFileSystemModel 上可能很有用,例如为不同类型的文件提供不同颜色的 BackgroundRole。

子类化的参考模型

模型子类需要提供 QAbstractItemModel 基类中定义的许多虚函数的实现。需要实现的这些函数的数量取决于模型的类型——它是提供具有简单列表、表或复杂项目层次结构的视图。从QAbstractListModel 和 QAbstractTableModel 继承的模型可以利用这些类提供的函数的默认实现。在树形结构中公开数据项的模型必须为 QAbstractItemModel 中的许多虚函数提供实现。

需要在模型子类中实现的函数可以分为三组:

  • 项数据处理:所有模型都需要实现功能,以允许视图和委托查询模型的维度、检查项和检索数据。
  • 导航和索引创建:分层模型需要提供视图可以调用的函数,以导航它们公开的树状结构,并获得项目的模型索引。
  • 拖放支持和 MIME 类型处理:模型继承了控制内部和外部拖放操作执行方式的函数。这些函数允许用其他组件和应用程序可以理解的 MIME 类型来描述数据项。

有关更多信息,请参阅 Qt4 c++ GUI编程的“项目视图类”一章(“Item View Classes” Chapter of C++ GUI Programming with Qt 4)。

项数据处理

模型可以提供对其提供的数据的不同级别的访问:它们可以是简单的只读组件,一些模型可能支持调整大小操作,而其他模型可能允许编辑项。

只读访问

为了提供对模型提供的数据的只读访问,以下函数必须在模型的子类中实现:

函数 说明
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 指定的许多不同类型的角色相关联的数据。在更改数据项之后,模型必须发出 datachchanged() 信号来通知其他组件该更改。
setHeaderData() 用于修改水平和垂直标题信息。在更改数据项之后,模型必须发出 headerdatachchanged() 信号来通知其他组件该更改。

可调整大小的模型

所有类型的模型都可以支持行插入和删除。表模型和层次模型还可以支持列的插入和删除。在模型维度发生更改之前和之后通知其他组件是很重要的。因此,可以实现以下函数来允许模型调整大小,但实现必须确保调用适当的函数来通知附加的视图和委托:

函数 说明
insertRows() 用于向所有类型的模型添加新的数据行和数据项。实现必须在将新行插入任何底层数据结构之前调用 beginInsertRows(),然后立即调用 endInsertRows()。
removeRows() 用于从所有类型的模型中删除行及其包含的数据项。实现必须在插入新列到任何底层数据结构之前调用 beginRemoveRows(),然后立即调用 endRemoveRows()。
insertColumns() 实现必须在从任何基础数据结构中删除行之前调用 beginInsertColumns(),然后立即调用 endInsertColumns()。
removeColumns() 实现必须在列从任何基础数据结构中删除之前调用 beginRemoveColumns(),然后立即调用 endRemoveColumns()。

但是,也可能有手术只取得部分成功的情况;例如,如果可以插入少于指定的行数。在这种情况下,模型应该返回 false,以指示无法启用任何附加组件来处理这种情况。

在调整 API 的实现中调用的函数发出的信号使附加组件有机会在任何数据不可用之前采取行动。使用 begin 和 end 函数封装插入和删除操作也使模型能够正确管理持久的模型索引。

通常,begin 和 end 函数能够通知其他组件模型基础结构的更改。对于模型结构更复杂的更改(可能涉及内部重组或数据排序),需要发出 layoutChanged() 信号以更新任何附加的视图。

模型数据的惰性填充

模型数据的惰性填充有效地允许延迟对模型信息的请求,直到视图实际需要它。

有些模型需要从远程数据源获取数据,或者必须执行耗时的操作来获取有关数据组织方式的信息。由于视图通常要求尽可能多的信息以准确地显示模型数据,因此限制返回给它们的信息数量以减少对数据的不必要的后续请求是有用的。

在层次模型中,查找给定项的子项数量是一项开销很大的操作,因此确保模型的rowCount()实现只在必要时被调用是很有用的。在这种情况下,可以重新实现hasChildren()函数,以提供一种廉价的方式让视图检查子项目的存在,在QTreeView的情况下,为其父项目绘制适当的装饰。

无论hasChildren()的重新实现返回真或假,视图可能不需要调用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)表示的公共格式提供项目内容是有用的。注意,图像、颜色和 HTML 文档可以通过QMimeData::setImageData()、QMimeData::setColorData() 和 QMimeData::setHtml() 函数轻松地添加到 QMimeData 对象中。

接受数据

当在视图上执行拖放操作时,将查询底层模型,以确定它支持的操作类型和它可以接受的 MIME 类型。这些信息是由 QAbstractItemModel::supportedDropActions() 和 QAbstractItemModel::mimeTypes() 函数提供的。不覆盖 QAbstractItemModel 提供的实现的模型支持复制操作和默认的项目内部MIME类型。

当序列化的项目数据被放到视图中时,数据将使用 QAbstractItemModel::dropMimeData() 的实现插入到当前模型中。这个函数的默认实现将永远不会覆盖模型中的任何数据;相反,它试图将数据项作为项的兄弟项或该项的子项插入。

为了利用QAbstractItemModel对内置MIME类型的默认实现,新模型必须提供以下函数的重新实现:

函数 说明
insertRows() 这些函数使模型能够使用 QAbstractItemModel::dropMimeData() 提供的现有实现自动插入新数据。
insertColumns()
setData() 允许用项目填充新行和列。
setItemData() 此函数为填充新项提供了更有效的支持。

为了接受其他形式的数据,这些函数必须重新实现:

函数 说明
supportedDropActions() 用于返回拖放操作的组合,指示模型接受的拖放操作的类型。
mimeTypes() 用于返回可由模型解码和处理的 MIME 类型列表。通常,模型中支持的输入 MIME 类型与它在为外部组件编码数据时所使用的 MIME 类型相同。
dropMimeData() 执行通过拖放操作传输的数据的实际解码,确定将其设置在模型的何处,并在必要时插入新的行和列。这个函数如何在子类中实现取决于每个模型所公开的数据的需求。

简单地调用子类中其他函数的重新实现是很有用的,比如 setData()、insertRows() 和 insertColumns(),以确保模型的行为一致。

为了确保拖动操作正常工作,重新实现以下从模型中删除数据的函数是很重要的:

  • removeRows()
  • removeRow()
  • removeColumns()
  • removeColumn()

有关对项视图进行拖放的详细信息,请参阅对项视图进行拖放(Using drag and drop with item views)。

便利观点

便利视图(QListWidget、QTableWidget和QTreeWidget)覆盖了默认的拖放功能,以提供不那么灵活但更自然的行为,这适合于许多应用程序。例如,由于将数据拖放到 QTableWidget 中的单元格中,用正在传输的数据替换现有内容更为常见,因此底层模型将设置目标项的数据,而不是向模型中插入新的行和列。有关方便视图中拖放的更多信息,您可以看到使用项目视图的拖放。

针对大量数据的性能优化

canFetchMore() 函数检查父类是否有更多可用数据,并相应地返回 true 或 false。fetchMore() 函数根据指定的父类获取数据。这两个函数可以组合在一起,例如,在涉及增量数据的数据库查询中填充 QAbstractItemModel。我们重新实现 canFetchMore() 来指示是否有更多的数据需要获取,以及 fetchMore() 来根据需要填充模型。

另一个例子是动态填充的树模型,当树模型中的一个分支被展开时,我们重新实现 fetchMore()。

如果重新实现 fetchMore() 向模型添加行,则需要调用 beginInsertRows() 和 endInsertRows()。另外,canFetchMore() 和 fetchMore()都必须重新实现,因为它们的默认实现返回 false,不执行任何操作。

模型/视图类

这些类使用模型/视图设计模式,在这种模式中,底层数据(在模型中)与用户(在视图中)显示和操作数据的方式保持分离。

说明
QAbstractItemDelegate 用于显示和编辑模型中的数据项
QAbstractItemModel 项模型类的抽象接口
QAbstractItemView 项视图类的基本功能
QAbstractListModel 抽象模型,可以被子类化来创建一维列表模型
QAbstractProxyModel 代理项模型的基类,可以执行排序、过滤或其他数据处理任务
QAbstractTableModel 抽象模型,可以被子类化来创建表模型
QColumnView 为列视图建模/查看实现
QDataWidgetMapper 数据模型的一部分到小部件之间的映射
QFileSystemModel 本地文件系统的数据模型
QHeaderView 项目视图的标题行或标题列
QIdentityProxyModel 代理未修改的源模型
QItemDelegate 显示和编辑模型中的数据项的工具
QItemEditorCreator 使创建项编辑器创建器基而不子类化QItemEditorCreatorBase成为可能
QItemEditorCreatorBase 在实现新的项编辑器创建器时必须被子类化的抽象基类
QItemEditorFactory 用于编辑视图和委托中的项数据的小部件
QItemSelection 管理有关模型中所选项的信息
QItemSelectionModel 跟踪视图的选定项
QItemSelectionRange 管理有关模型中选定项目范围的信息
QListView 在模型上列出或图标视图
QListWidget 基于项列表小部件
QListWidgetItem 项,用于QListWidget项视图类
QModelIndex 用于在数据模型中定位数据
QPersistentModelIndex 用于在数据模型中定位数据
QSortFilterProxyModel 支持对在另一个模型和视图之间传递的数据进行排序和过滤
QStandardItem 项,用于QStandardItemModel类
QStandardItemEditorCreator 注册小部件而不必子类化QItemEditorCreatorBase的可能性
QStandardItemModel 用于存储自定义数据的通用模型
QStringListModel 向视图提供字符串的模型
QStyledItemDelegate 显示和编辑模型中的数据项的工具
QTableView 表视图的默认模型/视图实现
QTableWidget 带有默认模型的基于项的表视图
QTableWidgetItem 与QTableWidget类一起使用的项
QTableWidgetSelectionRange 在不使用模型索引和选择模型的情况下与模型中的选择交互的方法
QTreeView 树视图的默认模型/视图实现
QTreeWidget 使用预定义树模型的树视图
QTreeWidgetItem 与QTreeWidget便利类一起使用的项
QTreeWidgetItemIterator 遍历QTreeWidget实例中的项的方法

相关的例子

  • Dir View
  • Spin Box Delegate
  • Pixelator
  • Simple Tree Model
  • Chart

参见 Item Views Puzzle Example。

你可能感兴趣的:(C++,Qt,Qt模型视图,c++,qt)