模型/视图编程简介
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, ...);
为列表和表等简单的单层数据结构提供接口的模型不需要提供任何其他信息,但是,如上面的代码所示,在获取模型索引时,我们需要提供更多的信息。
行和列
该图显示了基本表模型的表示形式,其中每个项都由一对行号和列号定位。通过将相关的行号和列号传递给模型,我们获得了一个引用数据项的模型索引。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();
}
视图呈现模型的内容,通过模型的接口访问数据。当用户尝试编辑项时,视图使用默认代理提供编辑器小部件。
多个视图使用一个模型
为同一个模型提供多个视图只是为每个视图设置相同的模型而已。在下面的代码中,我们创建了两个表格视图,每个视图使用相同的简单表格模型,我们已经为这个例子创建:
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类的实例中。它为单个模型中的项维护模型索引,并且独立于任何视图。由于模型可以有多个视图,因此可以在视图之间共享选择,从而允许应用程序以一致的方式显示多个视图。
选择由选择范围组成。通过只记录所选项目的每个范围的开始和结束模型索引,这些方法有效地维护了关于大型项目选择的信息。项目的非连续选择是通过使用多个选择范围来描述选择来构造的。
选择应用于选择模型持有的模型索引集合。最近选择的应用项目称为当前选择。即使在应用了此选择之后,也可以通过使用某些类型的选择命令来修改此选择的效果。这些将在本节稍后讨论。
当前项和选定项
在视图中,始终存在一个当前项和一个选定项—两个独立的状态。项目可以是当前项目并同时被选中。视图负责确保始终存在当前项,例如,键盘导航需要当前项。
下表突出显示了当前项和选定项之间的差异。
在操作选择时,将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 ¤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命令来反转给定项目的选择状态。
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()函数用于确定任何给定项是否为另一级别项的父级。