模型视图(02):【纲】Model View Programming[官翻]

文章目录

Model View Programming

模型/视图编程简介

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是模型/视图框架中代理的抽象基类。默认的代理实现是由QStyledItemDelegate提供的,它被Qt的标准视图用作默认代理。不过,QStyledItemDelegate和QItemDelegate是为视图中的项绘制和提供编辑器的独立替代方案。它们之间的区别是QStyledItemDelegate使用当前样式绘制其项。因此,我们建议在实现自定义代理或使用Qt样式表时使用QStyledItemDelegate作为基类。

代理在代理类一节中进行了描述。

排序

在模型/视图架构中有两种处理排序的方法;选择哪种方法取决于您的底层模型。

如果你的模型是可排序的,也就是说,如果它重新实现了QAbstractItemModel::sort()函数,QTableView和QTreeView都提供了一个API,允许你以编程的方式对模型数据进行排序。另外,你可以通过将QHeaderView::sortIndicatorChanged()信号分别连接到QTableView::sortByColumn()插槽或QTreeView::sortByColumn()插槽来启用交互式排序(即允许用户通过点击视图的头对数据进行排序)。

如果您的模型没有所需的接口,或者您希望使用列表视图来表示数据,那么另一种方法是在在视图中表示数据之前使用代理模型来转换模型的结构。代理模型部分将对此进行详细介绍。

方便类

为了方便依赖于Qt的基于项目的项目视图和表类的应用程序,许多便利类是从标准视图类派生出来的。它们不打算被子类化。

此类类的示例包括QListWidget、QTreeWidget和QTableWidget。

这些类不如视图类灵活,并且不能用于任意模型。我们建议您使用模型/视图方法来处理项视图中的数据,除非您非常需要基于项的类集。

如果您希望利用模型/视图方法提供的特性,同时仍然使用基于项目的接口,可以考虑使用视图类,例如QListView、QTableView和QStandardItemModel。

使用模型和视图

下面的部分解释了如何在Qt中使用模型/视图模式,每个部分都包括一个示例,后面的部分展示了如何创建新的组件。

Qt包括的两种模型

Qt提供的两个标准模型是QStandardItemModel和QFileSystemModel。(QDirModel已经过时,不推荐使用)

  • QStandardItemModel是一种多用途模型,可用于表示列表表格视图所需的各种不同数据结构。这个模型还包含数据项。
  • QFileSystemModel是维护目录内容信息的模型。因此,它本身不包含任何数据项,而只是表示本地归档系统上的文件和目录。

QFileSystemModel提供了一个可以进行试验的现成模型,并且可以轻松地配置为使用现有数据。通过使用这个模型,我们可以展示如何设置一个使用现成视图的模型,并探索如何使用模型索引操作数据。

利用现有模型使用视图

QListView和QTreeView类是最适合与QFileSystemModel一起使用的视图。下面的示例在树视图中显示目录的内容,旁边是列表视图中的相同信息。视图共享用户的选择,因此选择的项目在两个视图中突出显示。

这示例展示了使用模型的最简单的方法。模型的构造和使用在一个main()函数中执行:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QSplitter *splitter = new QSplitter;
/* 我们设置了QFileSystemModel以便可以使用,并创建一些视图来显示目录的内容。
 * 该模型用于使用来自某个文件系统的数据。对setRootPath()的调用告诉模型文件系统上的哪个驱动器要公开给视图。*/
    QFileSystemModel *model = new QFileSystemModel;
    model->setRootPath(QDir::currentPath());
  
  /*  视图的构造方式与其他小部件相同。要设置一个视图来显示模型中的项,只需用目录模型作为参数调用它的setModel()函数。
    我们通过在每个视图上调用setRootIndex()函数来筛选模型提供的数据,并从文件系统模型中为当前目录传递合适的模型索引。
    本例中使用的index()函数是QFileSystemModel唯一的;我们为它提供一个目录,它返回一个模型索引。模型索引是在模型类中讨论的。*/
  
    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()));
  
  
    splitter->setWindowTitle("Two views onto the same file system model");
    splitter->show();
    return app.exec();
}

模型

在研究如何处理选择之前,您可能会发现检查模型/视图框架中使用的概念是有用的。

基本概念

在模型/视图体系结构中,模型提供了视图和代理用来访问数据的标准接口。在Qt中,标准接口是由QAbstractItemModel类定义的。无论数据项如何存储在任何底层数据结构中,QAbstractItemModel的所有子类都数据表示为包含数据项表的层次结构。视图使用此约定来访问模型中的数据项,但它们向用户显示信息的方式不受限制。

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

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

模型索引 Model indexes

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

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

QAbstractItemModel *model = index.model();

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

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

行和列

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

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

为列表和表等简单的单层数据结构提供接口的模型不需要提供任何其他信息,但是,如上面的代码所示,在获取模型索引时,我们需要提供更多的信息。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vc1CeBVv-1607992157679)(E:\Users\Desktop\modelview-treemodel.png)] 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定义。

我们可以通过传递与项目对应的模型索引向模型请求项目的数据,并通过指定一个角色来获得我们想要的数据类型:

QVariant value = model->data(index, role);

项目的角色

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

项目数据的最常见用途包括Qt::ItemDataRole中定义的标准角色。通过为每个角色提供适当的项数据,模型可以向视图和代理提供关于应该如何将项呈现给用户的提示。不同类型的视图可以根据需要自由地解释或忽略这些信息。还可以为特定于应用程序的目的定义其他角色。

总结

  • 模型索引以一种独立于任何底层数据结构的方式向视图和代理提供关于模型提供的项位置的信息。
  • 项目通过其行号和列号以及其父项目的模型索引来引用。
  • 模型索引由模型根据其他组件(如视图和代理)的请求构建。
  • 如果在使用index()请求索引时为父项指定了有效的模型索引,则返回的索引引用模型中该父项下的项。所获得的索引引用该项的子元素。
  • 如果使用index()请求索引时为父项指定了无效的模型索引,则返回的索引引用模型中的顶级项。
  • 角色区分与某项关联的不同类型的数据。

使用模型索引

为了演示如何使用模型索引从模型中检索数据,我们设置了一个没有视图的QFileSystemModel,并在小部件中显示文件和目录的名称。虽然这不是使用模型的常规方式,但它演示了模型在处理模型索引时使用的约定。

QFileSystemModel加载是异步的,以最小化系统资源的使用。在处理这个模型时,我们必须考虑到这一点。

我们通过以下方式构建文件系统模型:

     QFileSystemModel *model = new QFileSystemModel;
     connect(model, &QFileSystemModel::directoryLoaded, [model](const QString &directory) {
         QModelIndex parentIndex = model->index(directory);
         int numRows = model->rowCount(parentIndex);
     });
     model->setRootPath(QDir::currentPath);

在本例中,我们首先设置一个默认的QFileSystemModel。我们将它连接到一个lambda,在这个lambda中,我们将使用该模型提供的index()的特定实现来获得父索引。在lambda中,我们使用rowCount()函数计算模型中的行数。最后,我们设置QFileSystemModel的根路径,以便它开始加载数据并触发lambda。

为了简单起见,我们只对模型的第一列中的项目感兴趣。我们依次检查每一行,获取每行第一项的模型索引,并读取模型中为该项存储的数据。

     for (int row = 0; row < numRows; ++row) {
         QModelIndex index = model->index(row, 0, parentIndex);

为了获得模型索引,我们指定行号、列号(第一列为零)以及我们想要的所有项的父项的适当模型索引。使用模型的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以表格的形式显示模型中的项目,很像电子表格应用程序的布局。

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

使用一个模型

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

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 QStringListModel(numbers);

注意,QStringListModel声明为QAbstractItemModel。这允许我们使用模型的抽象接口,并确保代码仍然工作,即使我们用不同的模型替换字符串列表模型。

QListView提供的列表视图足以显示字符串列表模型中的项。我们用下面几行代码构建视图,并建立模型:

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

     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()函数来呈现它们自己的内容。但是,简单的基于widget的代理可以继承QStyledItemDelegate而不是QAbstractItemDelegate,并利用这些函数的默认实现。

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

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

使用现有的代理

Qt提供的标准视图使用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;
 };

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

提供一个编辑器

在本例中,当表视图需要提供一个编辑器时,它要求代理提供一个适合被修改项目的编辑器小部件。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角色中的数据,并相应地在spin框中设置值。

 void SpinBoxDelegate::setEditorData(QWidget *editor,
                                     const QModelIndex &index) const
 {
     int value = index.model()->data(index, 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为意外数据提供了合理的默认值。

更新编辑器的几何大小

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

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

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

编辑提示

编辑之后,代理应该向其他组件提供关于编辑过程结果的提示,并提供将帮助任何后续编辑操作的提示。这是通过发出带有适当提示的closeEditor()信号来实现的。这是由默认的QStyledItemDelegate事件过滤器处理的,这个过滤器是我们在构造旋转框时安装在它上面的。

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

另一种不需要使用事件过滤器的方法是提供我们自己的编辑器小部件,为方便起见,可以将QSpinBox子类化。这种替代方法可以让我们更好地控制编辑器小部件的行为,但代价是编写额外的代码。如果需要自定义标准Qt编辑器小部件的行为,那么在代理中安装事件过滤器通常更容易。

代理不必发出这些提示,但是不发出提示的代理与应用程序的集成较少,而且与发出提示以支持常见编辑操作的代理相比,它们的可用性更差。

处理项视图中的选定项

概念

项目视图类中使用的选择模型提供了基于模型/视图体系结构的工具的选择的一般描述。尽管操作选择的标准类对于所提供的项视图来说已经足够了,但是选择模型允许您创建专门的选择模型来满足您自己的项模型和视图的需求。

关于视图中选择的项的信息存储在QItemSelectionModel类的实例中。它为单个模型中的项维护模型索引,并且独立于任何视图。由于模型可以有多个视图,因此可以在视图之间共享选择,从而允许应用程序以一致的方式显示多个视图。

选择由选择范围组成。通过只记录所选项目的每个范围的开始和结束模型索引,这些方法有效地维护了关于大型项目选择的信息。项目的非连续选择是通过使用多个选择范围来描述选择来构造的。

选择应用于选择模型持有的模型索引集合。最近选择的应用项目称为当前选择。即使在应用了此选择之后,也可以通过使用某些类型的选择命令来修改此选择的效果。这些将在本节稍后讨论。

当前项和选定项

在视图中,始终存在一个当前项和一个选定项—两个独立的状态。项目可以是当前项目并同时被选中。视图负责确保始终存在当前项,例如,键盘导航需要当前项。

下表突出显示了当前项和选定项之间的差异。

当前项选定项
当前只能有一个项目。可以有多个选择项。
通过键导航或单击鼠标按钮,当前项将被更改。当用户与项目交互时,项目的选择状态是被设置或未设置的,这取决于几个预定义的模式——比如单选择、多选择等。
如果按下编辑键F2或双击当前项目(如果启用了编辑),当前项目将被编辑。当前项可以与锚一起使用,以指定应选择或取消选择的范围(或两者的组合)。
当前项由焦点矩形指示。选中的项目用选择矩形表示。

在操作选择时,将QItemSelectionModel看作一个项目模型中所有项目的选择状态的记录通常是有帮助的。一旦建立了选择模型,项目集合就可以被选择、取消选择,或者它们的选择状态可以被切换,而不需要知道哪些项目已经被选择了。可以随时检索所有被选中项的索引,并且可以通过信号和槽机制通知其他组件选择模型的变化。

使用选择模型

标准视图类提供了可以在大多数应用程序中使用的默认选择模型。可以使用视图的selectionModel()函数获得属于一个视图的选择模型,并通过setSelectionModel()在多个视图之间共享,因此通常不需要构造新的选择模型。

通过指定模型和QItemSelection的一对模型索引来创建选择。它使用索引来引用给定模型中的项,并将它们解释为选定项块中的左上和右下项。若要将选择应用于模型中的项目,则需要将选择提交给选择模型;这可以通过多种方式实现,每一种方式都对选择模型中已经出现的选择产生不同的影响。

选定项

为了演示选择的一些主要特性,我们构造了一个包含32个条目的自定义表模型的实例,并打开一个表视图来显示它的数据:

     TableModel *model = new TableModel(8, 4, &app);

     QTableView *table = new QTableView(0);
     table->setModel(model);

     QItemSelectionModel *selectionModel = table->selectionModel();

检索表视图的默认选择模型,以供以后使用。我们不修改模型中的任何项,而是选择视图将在表的左上方显示的一些项。为此,我们需要检索待选区域中左上角和右下角项目对应的模型索引:

     QModelIndex topLeft;
     QModelIndex bottomRight;

     topLeft = model->index(0, 0, QModelIndex());
     bottomRight = model->index(5, 2, QModelIndex());

要在模型中选择这些项目,并在表视图中看到相应的变化,我们需要构造一个选择对象,然后将其应用到选择模型中:

     QItemSelection selection(topLeft, bottomRight);
     selectionModel->select(selection, QItemSelectionModel::Select);

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

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

读取选择状态

存储在选择模型中的模型索引可以使用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 : qAsConst(items)) {
         QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
         model->setData(index, text);
     }

     items = deselected.indexes();

     for (const QModelIndex &index : qAsConst(items)) {
         model->setData(index, QString());
 }

我们可以通过将currentChanged()信号连接到使用两个模型索引调用的槽来跟踪当前关注的项。它们对应于先前的重点项目和当前的重点项目。

在下面的代码中,我们提供了一个接收currentChanged()信号的槽,并使用提供的信息来更新QMainWindow的状态栏:

 void MainWindow::changeCurrent(const QModelIndex &current,
     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命令来反转给定项目的选择状态。

     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组合的命令,则会选择包含引用的项的整个行。下面的代码演示了行和列标志的使用:

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

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

在示例模型上执行的命令都涉及到积累模型中选择的项。也可以清除选择,或者用新的选择替换当前的选择。

若要用新选择替换当前选择,请将其他选择标志与当前标志结合使用。使用此标志的命令指示选择模型将其当前的模型索引集合替换为select()调用中指定的索引。要在开始添加新选项之前清除所有选项,请将其他选择标记与清除标记组合在一起。这样做的效果是重置选择模型的模型索引集合。

选择模型中的所有项

要选择模型中的所有项,有必要为模型的每个级别创建一个选择,该级别覆盖了该级别中的所有项。我们通过检索与给定父索引对应的左上和右下项的索引来实现:

     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()函数来提供关于header的信息:

 QVariant StringListModel::headerData(int section, Qt::Orientation orientation,
                                      int role) const
 {
     if (role != Qt::DisplayRole)
         return QVariant();

     if (orientation == Qt::Horizontal)
         return QStringLiteral("Column %1").arg(section);
     else
         return QStringLiteral("Row %1").arg(section);
 }

同样,只有当角色是我们支持的角色时,我们才返回有效的QVariant。在决定返回的确切数据时,也要考虑报头的方向。
并非所有视图都显示带有项数据的标题,而那些显示标题的视图可以配置为隐藏它们。尽管如此,建议您实现headerData()函数来提供关于模型提供的数据的相关信息。

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

一个可编辑的模型

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

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

使模型可编辑

代理在创建编辑器之前检查项目是否可编辑。模型必须让代理知道它的项是可编辑的。我们通过为模型中的每个项返回正确的标志来实现这一点;在这种情况下,我们启用了所有项目,并使它们既可选择又可编辑:

 Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const
 {
     if (!index.isValid())
         return Qt::ItemIsEnabled;

     return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
 }

请注意,我们不必知道代理如何执行实际的编辑过程。我们只需要为代理提供一种方法来设置模型中的数据。这是通过setData()函数实现的:

 bool StringListModel::setData(const QModelIndex &index,
                               const QVariant &value, int role)
 {
     if (index.isValid() && role == Qt::EditRole) {

         stringList.replace(index.row(), value.toString());
         emit dataChanged(index, index, {role});
         return true;
     }
     return false;
 }

在这个模型中,字符串列表中与模型索引相对应的项被提供的值替换。但是,在修改字符串列表之前,我们必须确保索引是有效的,项是正确的类型,并且角色是受支持的。按照惯例,我们坚持该角色为EditRole,因为这是标准项代理使用的角色。然而,对于布尔值,你可以使用Qt::CheckStateRole并设置Qt::ItemIsUserCheckable标志;然后使用复选框编辑值。此模型中的基础数据对于所有角色都是相同的,因此此细节使得将模型与标准组件集成更加容易。

设置数据后,模型必须让视图知道某些数据已经更改。这是通过发送dataChanged()信号来完成的。由于只有一项数据发生了变化,信号中指定的项的范围仅限于一个模型索引。

此外,需要更改data()函数以添加Qt::EditRole测试:

 QVariant StringListModel::data(const QModelIndex &index, int role) const
 {
     if (!index.isValid())
         return QVariant();

     if (index.row() >= stringList.size())
         return QVariant();

     if (role == Qt::DisplayRole || role == Qt::EditRole)
         return stringList.at(index.row());
     else
         return QVariant();
 } 

插入和删除行

可以更改模型中的行数和列数。在字符串列表模型中,只有更改行数才有意义,因此我们只重新实现插入和删除行的函数。这些在类定义中声明:

bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;

因为这个模型中的行对应于列表中的字符串,所以insertRows()函数会在字符串列表中指定的位置之前插入一些空字符串。插入的字符串数等于指定的行数。

父索引通常用于确定应该在模型中的何处添加行。在本例中,我们只有一个顶级字符串列表,所以我们只向该列表中插入空字符串。

 bool StringListModel::insertRows(int position, int rows, const QModelIndex &parent)
 {
     beginInsertRows(QModelIndex(), position, position+rows-1);

     for (int row = 0; row < rows; ++row) {
         stringList.insert(position, "");
     }

     endInsertRows();
     return true;
 }

模型首先调用beginInsertRows()函数来通知其他组件行数即将发生变化。该函数指定要插入的第一个和最后一个新行的行号,以及它们的父项的模型索引。在更改字符串列表之后,它调用endInsertRows()来完成操作,并通知其他组件模型的维度已经更改,返回true表示成功。

从模型中删除行的函数编写起来也很简单。要从模型中删除的行由给定的位置和行数指定。为了简化实现,我们忽略了父索引,只从字符串列表中删除相应的项。

 bool StringListModel::removeRows(int position, int rows, const QModelIndex &parent)
 {
     beginRemoveRows(QModelIndex(), position, position+rows-1);

     for (int row = 0; row < rows; ++row) {
         stringList.removeAt(position);
     }

     endRemoveRows();
     return true;
 }

beginRemoveRows()函数总是在删除任何基础数据之前调用,并指定要删除的第一行和最后一行。这允许其他组件在数据不可用之前访问它。删除行之后,模型发出endRemoveRows()来完成操作,并让其他组件知道模型的维度已经更改。

下一个步骤

我们可以使用QListView类以垂直列表的形式显示模型项,从而显示这个模型或任何其他模型提供的数据。对于字符串列表模型,此视图还提供了一个默认编辑器,以便可以对项进行操作。我们在视图类中检查由标准视图类提供的可能性。

模型子类化参考文档更详细地讨论了QAbstractItemModel子类的需求,并提供了必须实现的虚函数的指南,以在不同类型的模型中启用各种特性。

项目视图便利类

基于项目的小部件具有反映其用途的名称:QListWidget提供项目列表,QTreeWidget显示多层次的树结构,QTableWidget提供单元格项目表。每个类继承QAbstractItemView类的行为,该类实现了用于项目选择和标题管理的通用行为。

列表小部件

单个级别的项目列表通常使用QListWidget和大量QListWidgetItems来显示。列表小部件的构造方式与其他任何小部件相同:

QListWidget *listWidget = new QListWidget(this);

当列表项被构造时,可以直接添加到列表小部件:

     new QListWidgetItem(tr("Sycamore"), listWidget);
     new QListWidgetItem(tr("Chestnut"), listWidget);
     new QListWidgetItem(tr("Mahogany"), listWidget);

它们也可以在没有父列表部件的情况下构造,并在稍后添加到列表中:

     QListWidgetItem *newItem = new QListWidgetItem;
     newItem->setText(itemText);
     listWidget->insertItem(row, newItem);

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

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

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

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

树小部件

QTreeWidget和QTreeWidgetItem类提供了树或层次结构的项目列表。tree小部件中的每个项目都可以有自己的子项目,并且可以显示许多列信息。树小部件创建就像任何其他小部件:

 QTreeWidget *treeWidget = new QTreeWidget(this);

在将项目添加到tree部件之前,必须设置列数。例如,我们可以定义两列,并创建一个头,在每列的顶部提供标签:

     treeWidget->setColumnCount(2);
     QStringList headers;
     headers << tr("Subject") << tr("Default");
     treeWidget->setHeaderLabels(headers);

为每个部分设置标签的最简单方法是提供一个字符串列表。对于更复杂的header,您可以构造一个树项目,按照您的意愿对其进行装饰,并将其用作tree小部件的header。

tree小部件中的顶级项是用tree小部件作为它们的父小部件构造的。它们可以按任意顺序插入,也可以通过在构造每个项时指定前一项来确保它们按特定的顺序列出:

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

树小部件处理顶层项目与处理树中更深层次的其他项目略有不同。可以通过调用tree小部件的takeTopLevelItem()函数从树的顶层删除项目,但是通过调用父级项目的takeChild()函数来删除较低层的项目。使用insertTopLevelItem()函数将项目插入树的顶层。在树的较低级别上,使用父项的insertChild()函数。

在树的顶层和低层之间移动项目很容易。我们只需要检查这些项是否为顶级项,这些信息由每个项的父()函数提供。例如,我们可以移除树小部件中的当前项目,而不管它的位置:

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

在tree小部件的其他地方插入项目遵循相同的模式:

     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)控制。此属性控制用户是否可以选择一个或多个项,以及在多个项选择中,所选项是否必须是一个连续的项范围。选择模式对上述所有小部件的工作方式相同。

image-20201210234856096

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

image-20201210234934461

  • 多项目选择:在此模式下,用户可以在不改变现有选择的情况下切换小部件中任何项目的选择状态,就像独立切换非独占复选框一样。

image-20201210235006053

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

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

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

     for (QTableWidgetItem *item : selected) {
         bool ok;
         double value = item->text().toDouble(&ok);

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

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

搜索

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

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

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

     for (QTreeWidgetItem *item : found) {
         item->setSelected(true);
         // Show the item->text(0) for each item.
     }

如果tree小部件中包含搜索字符串中给出的文本,则上述代码将导致选择这些项目。此模式还可以在列表和表小部件中使用。

对项目视图使用拖放

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

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

只允许从视图中导出项而不允许将数据拖放到其中的模型的需求比完全启用的拖放模型的需求要少。

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

使用方便的视图

QListWidget、QTableWidget和QTreeWidget使用的每种类型的项默认配置为使用一组不同的标志。例如,每个QListWidgetItem或QTreeWidgetItem最初都是启用的、可检查的、可选择的,并且可以用作拖放操作的源;还可以编辑每个QTableWidgetItem,并将其用作拖放操作的目标。

尽管所有的标准项目都设置了一个或两个拖放标志,你通常需要在视图本身设置各种属性来利用内置的拖放支持:

  • 若要启用项目拖动,请将视图的dragEnabled属性设置为true。
  • 为了允许用户在视图中删除内部或外部项目,请将视图的viewport()的acceptDrops属性设置为true。
  • 要向用户显示当前被拖放的项目如果被拖放会放在哪里,设置视图的showDropIndicator属性。这为用户提供了关于视图中项目位置的持续更新信息。

例如,我们可以使用以下代码来启用列表小部件的拖放功能:

 QListWidget *listWidget = new QListWidget(this);
 listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
 listWidget->setDragEnabled(true);
 listWidget->viewport()->setAcceptDrops(true);
 listWidget->setDropIndicatorShown(true);

结果是一个列表小部件,它允许在视图中复制项目,甚至允许用户在包含相同类型数据的视图之间拖动项目。在这两种情况下,项目都是复制而不是移动的。

为了让用户在视图中移动项目,我们必须设置列表小部件的dragDropMode:

 listWidget->setDragDropMode(QAbstractItemView::InternalMove); 

使用模型/视图类

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

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

注意,自定义数据类型必须声明为元对象,并且必须为它们实现流操作符。有关详细信息,请参阅QMetaObject类描述。

将已删除的数据插入到模型中

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

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

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

通过重新实现QAbstractItemModel::canDropMimeData(),模型可以禁止删除某些项,或者依赖于删除的数据。

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

 bool DragDropListModel::canDropMimeData(const QMimeData *data,
     Qt::DropAction action, int row, int column, const QModelIndex &parent)
 {
     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;

如果提供的数据不是纯文本,或者为drop提供的列号无效,那么一个简单的单列字符串列表模型可以指示失败。
将插入到模型中的数据根据是否将其拖放到现有项上而得到不同的处理。在这个简单的示例中,我们希望允许在现有项之间、列表中的第一项之前和最后一项之后的drop。

当发生删除时,对应于父项的模型索引将是有效的,表明删除发生在项上,或者它将是无效的,表明删除发生在视图中对应于模型顶层的某个位置。

     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 : qAsConst(newItems)) {
         QModelIndex idx = index(beginRow, 0, QModelIndex());
         setData(idx, text);
         beginRow++;
     }

     return true;
 }

注意,模型通常需要提供QAbstractItemModel::insertRows()和QAbstractItemModel::setData()函数的实现。

代理模型

在模型/视图框架中,单个模型提供的数据项可以由任意数量的视图共享,而且每个视图都可能以完全不同的方式表示相同的信息。自定义视图和代理是为相同数据提供完全不同表示的有效方法。但是,应用程序通常需要为相同数据的处理版本提供常规视图,例如对项列表进行不同排序的视图。

尽管将排序和过滤操作作为视图的内部功能来执行似乎是合适的,但是这种方法不允许多个视图共享此类潜在的昂贵操作的结果。另一种方法涉及到模型本身的排序,它会导致类似的问题,即每个视图都必须显示根据最近的处理操作组织的数据项。

为了解决这个问题,模型/视图框架使用代理模型来管理在单个模型和视图之间提供的信息。代理模型是从视图的角度来看,其行为类似于普通模型,并代表该视图从源模型访问数据。模型/视图框架使用的信号和槽确保每个视图都得到了适当的更新,无论在其自身和源模型之间放置了多少代理模型。

使用代理模型

代理模型可以插入到现有模型和任意数量的视图之间。Qt提供了一个标准的代理模型QSortFilterProxyModel,它通常被实例化并直接使用,但是也可以被子类化来提供定制的过滤和排序行为。QSortFilterProxyModel类可以按照以下方式使用:

     QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(parent);
     filterModel->setSourceModel(stringListModel);

     QListView *filteredView = new QListView;
     filteredView->setModel(filterModel);

因为代理模型继承了QAbstractItemModel,所以它们可以连接到任何类型的视图,并且可以在视图之间共享。它们还可以用于处理管道安排中从其他代理模型获得的信息。

QSortFilterProxyModel类被设计用来在应用程序中实例化和直接使用。通过将这些类子类化并实现所需的比较操作,可以创建更专门化的代理模型。

定制代理模型

通常,代理模型中使用的处理类型涉及到将每个数据项从源模型中的原始位置映射到代理模型中的不同位置。在某些模型中,某些项可能在代理模型中没有相应的位置;这些模型正在过滤代理模型。使用代理模型提供的模型索引查看访问项,这些索引不包含关于源模型或模型中原始项位置的信息。

QSortFilterProxyModel允许在将源模型中的数据提供给视图之前对其进行筛选,还允许将源模型的内容作为预先排序的数据提供给视图。

自定义过滤模型

QSortFilterProxyModel类提供了一个非常通用的过滤模型,可以在各种常见情况下使用。对于高级用户,QSortFilterProxyModel可以被子类化,提供一种机制来实现定制的过滤器。

QSortFilterProxyModel的子类可以重新实现两个虚拟函数,当来自代理模型的模型索引被请求或使用时,它们会被调用:

  • filterAcceptsColumn()用于从部分源模型中筛选特定的列。
  • filterAcceptsRow()用于从部分源模型中筛选特定的行。

QSortFilterProxyModel中上述函数的默认实现都返回true以确保所有的项都被传递到视图中;这些函数的重新实现应该返回false以过滤出单独的行和列。

自定义排序模型

QSortFilterProxyModel实例使用std::stable_sort()函数来设置源模型和代理模型中的项之间的映射,允许在不修改源模型结构的情况下将排序后的项层次结构暴露给视图。要提供自定义排序行为,请重新实现lessThan()函数来执行自定义比较。

子类化的参考模型

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

模型子类中需要实现的功能可分为三组:

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

项数据处理

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

只读访问

要以只读方式访问模型提供的数据,必须在模型的子类中实现以下功能:

方法简介
flags()由其他组件使用,以获取关于模型提供的每一项的信息。在许多模型中,标志的组合应该包括Qt::ItemIsEnabled和Qt::ItemIsSelectable。
data()用于向视图和委托提供项数据。一般来说,模型只需要为Qt::DisplayRole和任何特定于应用程序的用户角色提供数据,但为Qt::ToolTipRole、Qt:: accessiblet外向和Qt::AccessibleDescriptionRole提供数据也是一个很好的实践。有关与每个角色关联的类型的信息,请参阅Qt::ItemDataRole enum文档。
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函数封装insert和remove操作还使模型能够正确地管理持久模型索引。

通常,begin和end函数能够将模型底层结构的更改通知其他组件。对于模型结构的更复杂的变化,可能涉及内部重组、数据排序或任何其他结构变化,有必要执行以下顺序:

  • 发出layoutAboutToBeChanged()信号
  • 更新表示模型结构的内部数据。
  • 使用changePersistentIndexList()更新持久索引
  • 发出layoutChanged()信号。

这个序列可以用于任何结构更新,而不是更高级、更方便的受保护方法。例如,如果一个200万行的模型需要删除所有奇数行,那就是100万折减范围,每个折减范围为1个元素。使用beginRemoveRows和endRemoveRows 100万次是可能的,但这显然是低效的。相反,这可以被表示为一次布局更改,一次更新所有必需的持久索引。

模型数据的惰性填充

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

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

在层次模型中,查找给定项的子元素的数量是一项昂贵的操作,因此确保只在必要时调用模型的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类型(如文本/纯格式和图像/png)表示的通用格式提供项的内容是很有用的。注意,可以通过QMimeData::setImageData()、QMimeData::setColorData()和QMimeData::setHtml()函数轻松地将图像、颜色和HTML文档添加到QMimeData对象中。

接受放下的数据

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

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

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

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

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

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

如果dropMimeData()函数的实现通过插入或删除行或列来更改模型的维数,或者如果数据项被修改,则必须注意确保所有相关信号都被发出。只需调用子类中其他函数的重新实现,比如setData()、insertRows()和insertColumns(),就可以确保模型的行为一致。

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

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

有关使用项视图进行拖放的详细信息,请参阅使用项视图进行拖放。

方便的视图

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

针对大量数据的性能优化

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

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

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

模型/视图类

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

简介
QAbstractItemDelegate用于显示和编辑模型中的数据项
QAbstractItemModel项目模型类的抽象接口
QAbstractItemView项目视图类的基本功能
QAbstractListModel抽象模型,可以被子类化以创建一维列表模型
QAbstractProxyModel代理项模型的基类,可以执行排序、过滤或其他数据处理任务
QAbstractTableModel抽象模型,可以被子类化以创建表模型
QColumnView列视图的模型/视图实现
QConcatenateTablesProxyModel代理多个源模型,并连接它们的行
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。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值