QT包含了 item view 类,这些类,使用模型/视图架构来管理数据与数据呈现方式之间的关系。这种架构引入了功能的分离,给程序员带来了非常大的灵活性来自定义数据的展示,也提供了一个标准模型接口,使得很多不同的数据源都能被现有的视图所使用。在这篇文章中,我们对模型/视图范例,与之相关的概念做了一个简单介绍,并且也描述了数据视图架构。这个架构中的每一部分都会被解释到,也提供了很多例子来讲解怎么使用架构中提供的类。
模型-视图-控制器(MVC)是一种设计模式,起源于一种经常被用来构建用户界面程序的语言,Smalltalk。在 Design Patterns中,Gamma et al. 写到:
MVC由3种对象组成。模型(M)是应用对象,视图(V)是界面显示,控制器(C)定义了用户界面对用户输入的响应。MVC之前,用户界面的设计倾向于把这些对象融合到一起。MVC把他们分离开增强了灵活性以及可重用性。
如果把视图跟控制器结合在一起,就形成了模型/视图架构。这种架构仍然是把数据的存储方式与数据的显示方式分离的,但是提供了一个基于同样原理的更简单的一个框架。这种分离特性使得可以在不同视图中显示相同的数据,并且可以在不改变之前的数据结构的情况下实现新的视图类型。为了允许灵活的处理用户的输入,我们引入了委托的概念。这个框架拥有委托的好处是他可以允许用户自定义数据渲染和编辑的方式。
模型/视图架构 模型与数据源通信,提供一个接口给架构中的其他部分。通信的本质依赖于数据源的类型,和实现模型的方式。 视图从模型中获取到模型索引;这些索引是对数据的引用。通过模型提供的模型索引,视图可以从数据源中获取数据。 在标准视图中,委托用来渲染数据。当一个数据被编辑时,委托通过模型索引直接与模型通信。 |
通常,模型/视图类可以被分成上述描述的三个部分:模型,试图和委托。这几个部分每一个都通过一个抽象类来定义,这些抽象类提供了一些通用接口,在某些情况下,也会提供一个默认特性的实现。为了提供其他组成部分所需要的所有功能,这些抽象类需要被继承;这也允许了一些特有组件的实现。
模型,视图和委托彼此之间通过信号和槽来进行通信。
模型中的信号通知视图数据源中数据的改变。
视图中的信号提供了用户与显示的数据之间的交互信息。
委托中的信号通常在编辑时被使用,用来告知模型和视图编辑器的状态。
所有数据模型都是基于QAbstractItemModel 实现的。这个类定义了视图和委托用来访问数据的接口。数据本身并不需要存储在模型当中;它可以存在于一个数据结构中,也可以存在于一个被单独的类,文件,数据库或其他应用组件提供的仓库中。
围绕着模型的基本概念都展示在Model Classes中。
QAbstractItemModel提供了一个接口,这个接口可以灵活的处理以列表,表格和树的形式展示数据的视图。然而,当给列表,表格这样的数据结构实现模型的时候,QAbstractListModel 和 QAbstractTableModel是一个更好的起点,因为他们提供了很多通用函数的默认实现。这些类的每一种都可以被继承,用来实现支持特有的列表,表格种类的模型。
模型子类化的过程在Creating New Models讨论。
QT提供了一些现成的模型用来处理数据:
QStringListModel 用来保存一列QString项。
QStandardItemModel 管理更复杂是树形数据结构,每一个条目都可以保存任意数据。
QFileSystemModel 提供了本地文件系统的文件和目录信息。
QSqlQueryModel, QSqlTableModel, 和 QSqlRelationalTableModel 通过模型/视图的约定规则来访问数据库。
如果这些标准模型不能满足你的需求,你可以继承 QAbstractItemModel, QAbstractListModel, 或 QAbstractTableModel 来创建自定义模型。
QT提供了一些不同的视图的完整实现: QListView显示一列条目, QTableView显示来自于表格模型的数据,QTreeView以一个层级的列表形式来显示模型数据。这些类的每一个都是基于抽象基类QAbstractItemView实现的。虽然这些类的实现是现成可用的,但是你仍然可以通过继承它们来实现自定义视图。
QAbstractItemDelegate 是在模型/视图框架中的一个抽象基类。 QStyledItemDelegate提供了默认的委托实现,可以被标准视图用作默认委托。QStyledItemDelegate 和 QItemDelegate用来构造和为视图中的条目提供编辑器的两个独立方案。它们中的区别是, QStyledItemDelegate使用当前的风格来绘画出这些条目。因此我们建议使用 QStyledItemDelegate 作为基类当需要实现委托和使用QT样式表的时候。
在模型/视图中有两种排序方法;选择哪种排序方式取决于潜在的模型。
如果你的模型是可排序的,例如,如果它重写了 QAbstractItemModel::sort() 函数, QTableView 和 QTreeView都会提供你以编程方式来排序数据的API。另外也可以实现用户交互排序(例如:允许用户点击视图的头字段,来排序数据)通过把信号QHeaderView::sortIndicatorChanged()和槽 QTableView::sortByColumn() 或 QTreeView::sortByColumn() 分别连接。
有一个替代方案,如果你的模型并不提供要求的接口,或者你想使用列表视图来展示数据,你可以在视图中展示数据之前,通过一个代理模型转换你的模型的结构。这些细节在 [Proxy Models](http://doc.qt.io/qt-5/model-view-programming.html#proxy-models)描述。
A number of convenience classes are derived from the standard view classes for the benefit of applications that rely on Qt’s item-based item view and table classes. They are not intended to be subclassed.
这些类的例子包括QListWidget, QTreeWidget, 和 QTableWidget。
这些类相对于视图类缺乏灵活性,也不能被任意模型所使用。我们推荐使用模型/视图方式来处理视图中的数据,除非你坚持使用一组item-based 的类。
如果你希望利用模型/视图编程的优秀特性,也想使用基于 item-based 的接口,考虑使用视图类,例如: QListView, QTableView, 和 QTreeView with QStandardItemModel.
下面的各小节解释了在QT中该怎么使用模型/视图类。每一个小节都包含一个例子,紧接着的一个小节展示如何创建新的组件。
QT提供的两个标准模型是QStandardItemModel 和 QFileSystemModel。 QStandardItemModel 是一个可以用来展示列表,表格和树视图所需的很多不同数据结构的多用途模型。这个模型也可以持有数据项。QFileSystemModel是一个维护关于目录内容的信息的模型。因此它不持有任何数据项本身,它仅仅显示本地文件系统的文件和目录。
QFileSystemModel提供了一个现成的模型可以用,并且可以很容易的被配置来使用已存在的数据。使用这种模型,我们可以展示如何构造可以被现成的视图所使用的模型,并且探索如何通过模型索引来操作数据。
QListView 和 QTreeView 是使用 QFileSystemModel最合适的视图。下面的这个例子是在一个树形视图中显示目录的内容,旁边是以列表的形式显示相同的内容。这个视图共享用户是否选中的操作,因此被选中的条目会在两个视图中都被高亮。
我们构造一个 QFileSystemModel使之可用,然后创建一些视图来显示这些目录内容。这展示了使用模型的最简单的方式。模型的构造和使用在main()函数中实现。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSplitter *splitter = new QSplitter;
QFileSystemModel *model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());
这个模型被设置用来使用某个文件系统来使用数据。 setRootPath() 的调用告知模型应该把哪一个驱动器显示给视图。
我们创建两个视图,以便于我们可以用两种不同方式来测试模型所持有的条目。
QTreeView *tree = new QTreeView(splitter);
tree->setModel(model);
tree->setRootIndex(model->index(QDir::currentPath()));
QListView *list = new QListView(splitter);
list->setModel(model);
list->setRootIndex(model->index(QDir::currentPath()));
使用构造其他窗口部件同样的方式来构造视图。设置一个视图来显示模型中的数据,就是一个简单的以目录模型为参数调用setModel() 函数的事情。在每一个视图中,我们通过调用setRootIndex()函数,同时给这个函数传递一个对应于当前目录的文件系统模型的模型索引来过滤模型中提供的数据。
这个例子中的index()函数对于 QFileSystemModel是唯一的。我们提供给它一个路径,它返回一个模型索引。模型索引在 Model Classes中会被讨论。
这个函数的剩余部分仅仅是设置在一个分割窗口部件中显示视图,并且运行应用程序的事件循环。
splitter->setWindowTitle("Two views onto the same file system model");
splitter->show();
return app.exec();
}
在上述例子中我们并没有提及如何处理数据条目的选中。这个主题的详细内容将在Handling Selections in Item Views小节中讨论。
在检验选中操作是怎么被处理之前,你也许会发现检验被用在模型/视图框架中的概念是有用的。
在模型/视图架构中,模型提供了一个被视图和委托用来访问数据的标准接口。在QT中,标准接口被QAbstractItemModel定义。无论数据条目是怎么存储在任何底层数据结构中的,所有QAbstractItemModel类的子类都把数据展示成一个包含多个条目表格的层级结构。视图用这种惯例来访问模型中的数据条目,但是他们没有被限制也用这种方式来把信息显示给用户。
模型也会通过信号和槽机制来通知关联的视图关于数据的改变。
这一描述了一些基本概念,这些概念是其他组件通过模型类访问数据条目的方法的核心。更多的高级概念将会在之后的小节被讨论。
为了确保数据的展示与数据被访问的方式保持分离,引入了模型索引的概念。每一条能够通过模型被获取的信息都被模型索引表示。视图和委托通过这些索引去请求要显示的数据。
因此,只有模型需要知道如何获取数据的,并且被模型所管理的数据的类型也都可以被普遍的定义。模型索引包含了一下指向创建它们的模型的指针,这就避免了当使用多个模型时会出现混淆。
QAbstractItemModel *model = index.model();
模型索引提供了对信息的临时引用,这些引用可以通过模型来获取和修改数据。因为模型有时会重组内部的数据结构,因此模型索引可能会失效,所以这些索引不应该被存储。如果要求对一条信息长期引用,就必须创建一个持久化模型索引。这提供了一个模型维持的信息的最新引用。临时模型索引由 QModelIndex类提供,持久化模型索引由QPersistentModelIndex 类提供。
为了获得与一个数据条目对应的模型索引,模型需指定3个属性:行数,列数和父条目的模型索引。下面的小节详细描述并且解释了这些属性。
在最基本的形式中,模型能够以一个简单的表格的形式被访问,表格中的数据项是通过行数和列数来定位的。这并不意味着底层的数据是以二维数组的结构来存储的;行数和列数仅仅是为了允许组件之间互相通讯的一个规范。通过给模型指定行数和列数我们可以获取任何已有的数据项的信息,并且我们会收到一个表示这个数据项的索引。
QModelIndex index = model->index(row, column, ...);
给简单的,单一的类似列表和表格这样的数据结构提供接口的模型不需要任何其他的信息被提供,但是就像上面代码表明的那样,当获取一个模型索引时,我们需要提供更多的信息。
当在列表和表格视图中使用数据时,模型提供的这种表格似的接口是很理想的;行数和列数系统准确的与视图显示数据项的方式相映射。然而,对于树型视图这样的结构要求模型对它内部的数据项引入一种更灵活的接口。因此,每一个数据项都可能是一个表格项的父节点,同样的,在树形视图顶层的数据项也可能包含一个列表项。
当请求一个模型数据项的索引时,我们必须提供一些关于这个数据项父节点的信息。在模型之外,访问数据项的唯一方式就是通过模型索引,因此必须提供父节点的模型索引。
QModelIndex index = model->index(row, column, parent);
模型中的数据项可以为其他组件行驶多种角色,允许在不同场景提供不同种类的数据。例如, Qt::DisplayRole习惯于获取以文本形式显示在视图中的字符文本串。通常, 数据项都包含有多种不同角色的数据,标准角色由 Qt::ItemDataRole定义。
我们可以提供一个与数据项对应的索引,和数据项的角色来要求模型获取数据项的数据。
QVariant value = model->data(index, role);
数据项角色 角色是为了向模型表明涉及的是什么种类的数据。视图可以以不同的方式来显示不同的角色,因此对于每一种角色提供适当的信息是相当重要的。 Creating New Models 一节中,详细涵盖了一些角色的特定用法。 |
关于数据项角色的普通用法都在 Qt::ItemDataRole定义的标准角色中涵盖。通过给每种角色提供适当的项数据,模型可以给视图和委托提供一些关于如何向用户展示数据项的提示。不同种类的视图可以根据需要自由的解释或忽略这些信息。它也可以根据特定应用的意图来定义额外的数据项角色。
模型索引告知了视图和委托关于模型提供的数据项的位置信息,在某种程度上,这些位置信息与底层的数据结构是独立无关的。
数据项可以通过行数和列数,以及它的父项的模型索引来指定。
模型索引是应其他(如视图和委托)组件的请求,由模型构造出来的。
当使用index()函数来请求模型索引时,指定了一个有效的父项模型索引,函数返回的索引所涉及的数据项就是参数指定的父项的子节点。
当使用index()函数来请求模型索引时,指定了一个无效的父项模型索引,函数就会返回一个顶层数据项的索引。
为了证明通过模型索引如何从模型中取得数据,我们建立一个QFileSystemModel ,并且通过一个窗口部件而不是视图来显示所有的文件名和目录名。虽然这并不是使用模型的通常用法,但是这展示了在处理模型索引时模型的使用约定。
我们通过下面的方式构建了一个文件系统模型:
QFileSystemModel *model = new QFileSystemModel;
QModelIndex parentIndex = model->index(QDir::currentPath());
int numRows = model->rowCount(parentIndex);
在这个例子中,我们建立一个默认的 QFileSystemModel,通过QFileSystemModel提供的一个特定的index()实现获取一个父节点索引,然后我们通过 rowCount() 函数计算模型中的行数。
为了简单起见,我们只对模型中第一列感兴趣。我们一次检查每一行,获取每一行中第一个数据项的模型索引,并且读取与这个数据项相关的数据。
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提供的标准接口来实现。在 Creating New Models小节中,我们通过创建持有多个文本列的简单现成可用的模型来证明。
在模型/视图框架中,视图从模型中获取数据,并把数据展示给用户。展示数据的方式不需要与模型提供的数据展示方式一样,并且可能与储存数据的底层数据结构完全不同。
数据和显示的分离通过QAbstractItemModel提供的标准模型接口,QAbstractItemView提供的标准视图接口,和以一种通用方式展示数据的模型索引来实现。视图通常管理从模型中获取的数据的整体布局。他们可以自己来渲染数据项,也可以利用委托来处理渲染和编辑特性。
不但展示数据,视图也处理数据项之间的导航,和数据项选择的某些方面。视图也实现了基本的用户交互功能,比如右键菜单,拖,拽操作。视图可以给数据项提供默认编辑功能,也可以通过委托来提供自定义的编辑功能。
虽然视图构造时可以不需要模型,但是它要显示有用信息之前,必须提供模型。视图可以通过使用selections记录用户选中的数据项,这些selections可以被每个视图单独维护,也可以在多个视图间共享。
像 QTableView和 QTreeView这样的一些视图,也把头字段作为数据项来显示。这些被视图类QHeaderView实现。头部也通常访问与包含这些头部的视图相关的同样模型。它们通常使用 QAbstractItemModel::headerData()函数来从模型中获取数据,并且通常以标签的形式显示头部信息。新的头部可以继承 QHeaderView来为视图提供更专业的标签。
QT提供了3个现成可用的视图类,这些视图类以大多数用户熟悉的方式来展示模型中的数据。QListView 可以把模型中的数据项展示为一个简单的列表,或者经典的图标视图的形式。 QTreeView可以把模型中的数据项展示为一个层级的列表,允许以一种简洁的方式来展示深度嵌套的结构。 QTableView 可以把模型中的数据项展示为表格的形式,就像一个电子制表软件的布局一样。
上面显示的标准视图的默认行为对于大多数应用应该是足够的。他们提供基本的编辑功能,并且可以通过被定制来适应专业用户界面的需要。
我们采用我们创建的字符串列表模型来作为一个模型例子,用一些数据设置它,然后构造一个视图来显示模型的内容。这可以通过单个函数来实现。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// Unindented for quoting purposes:
QStringList numbers;
numbers << "One" << "Two" << "Three" << "Four" << "Five";
QAbstractItemModel *model = new StringListModel(numbers);
注意StringListModel被声明为一个 QAbstractItemModel。这允许我们使用模型的抽象接口,并且确保就算我们用其他不同的模型来替换这个字符串列表模型,我们的代码也可以照常工作。
为显示字符串列表模型中的数据项,使用QListView提供的列表视图是充足的。我们构造一个视图,并且通过下列代码来设置模型:
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()函数可以指定要替换的选择模型。当我们想给同一模型提供多个一致的视图时,要有对视图使用的选择模型有控制能力。
通常,除非你在子例化一个模型或视图,否则你就不需要直接操作选择的内容。然而,如果需要,选择模型的接口可以被访问,将会在Handling Selections in Item Views小节讨论。
虽然视图类自己默认提供的选择模型很方便,但是当我们对同一模型使用多个视图时,通常期望的是模型数据和用户选择在多个视图中要保持一致。因为视图类允许它们内部的选择模型可以被替换,通过下面的代码我们可以在视图之间实现一个统一的选择模型。
secondTableView->setSelectionModel(firstTableView->selectionModel());
第二个视图的选择模型通过第一个视图提供。现在两个视图对同一个选择模型都起作用,保持数据和选中的数据项同步。
在上面的例子中,两个相同类型的视图被用来显示同一种模型数据。然而,如果使用了两个不同类型的视图,则在每一个视图中,选中数据项的显示可能差别很大。例如,在表格视图中连续选中的数据项,在树形视图中可能就是零散显示的。
不像模型-视图-控制器模式那样,模型/视图的设计并不包括一个完全分离的部分用来管理用户交互。通常,视图的责任就是把模型数据展示给用户,并且处理用户输入。为了允许灵活的获得用户输入,交互的操作通过委托来实现。这些委托组件提供了输入功能,而且也负责在视图中渲染单独的数据项。控制委托的标准接口定义在QAbstractItemDelegate类中。
委托本身被期待可以通过paint()和sizeHint()函数来渲染它们的内容。然而一些可能继承自QItemDelegate而不是 QAbstractItemDelegate的基于窗口的委托使用这些函数的默认实现。
委托可以通过使用窗口部件或者直接处理事件的方式来实现管理编辑过程的编辑器。本小节稍后部分涵盖了第一种方法,也会在 Spin Box Delegate例子中展示。
Pixelator例子展示了如何创建一个定制的委托来为表格视图实现专业的渲染。
QT提供的标准视图使用 QItemDelegate实例来提供编辑功能。 默认委托接口的实现以很平常的风格在每种标准视图中渲染数据项。
所有的标准角色被标准视图使用的默认委托处理。这些标准角色在QItemDelegate文档中解释。
视图使用的委托可以通过itemDelegate() 函数返回。 setItemDelegate() 允许你为标准视图安装一个定制化的委托,而且当为一个定制化的视图设置委托时,这个函数尤其必要。
在这里被实现的委托使用 QSpinBox提供编辑功能,主要用于那些显示整数的模型。虽然为了这个目的我们设置了一个自定义的基于整数的模型 ,但是我们可以很容易的使用QStandardItemModel 代替。因为自定义的委托控制了数据输入。我们构造了一个表格视图来显示模型的内容,并且使用一个自定义的委托来实现编辑功能。
我们继承自QItemDelegate类,因为我们没必要写定制的显示函数。但是,我们仍然需提供管理编辑的函数。
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = 0);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const Q_DECL_OVERRIDE;
void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const Q_DECL_OVERRIDE;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_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;
}
注意,我们不需要维护一个指向编辑器组件的指针,因为当它不再需要时,视图将负责销毁它。
我们在编辑器上安装委托的默认事件过滤器来确保它提供用户期待的标准编辑快捷键。额外的快捷键可以添加到编辑器中允许更复杂的行为。这些将在 Editing Hints小节讨论。
视图确保编辑器的数据和形状可以通过我们在后面定义的函数而被正确设置呢。根据视图提供的模型索引我们可以创建不同的编辑器。例如,如果我们有一列整数和一列字符串,我们根据被修改的列可以返回一个 QSpinBox 或 QLineEdit。
委托必须提供一个函数来把模型数据拷贝到编辑器中。在这个例子中,我们读出以display role存储的数据,并且相应的在spin box中设置数据。
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);
}
在这个例子中,我们知道编辑器窗口部件是一个QSpinBox,但是我们本来是可以在模型中给不同类型数据提供不同编辑器的。在这种情况下,在访问编辑器的成员函数之前,我们需要把widget强转为合适的类型。
当用户在Spinbox中编辑数据完成时,视图请求委托通过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);
}
因为视图替委托管理编辑器窗口部件,所以我们只需要用编辑器提供的内容来更新模型。在这个例子中,我们确保Spinbox是最新的,并且通过指定的Index来用它包含的数据更新模型。
标准 QItemDelegate 类通过释放 closeEditor() 信号来通知视图它什么时候结束编辑。视图确保编辑器窗口部件被关闭并且销毁。在这个例子中我们只提供了简单的编辑功能,所以我们从不需要释放这个信号。
所有对数据的操作都通过QAbstractItemModel提供的接口被实现。虽然这使得委托从它操作的数据独立出来,但是为了使用某些类型的编辑器组件必须做出一些假设。在这个例子中,虽然我们假定模型总是包含整形数据,但是我们仍然可以用不同的模型来使用委托,因为QVariant为意想不到的数据提供合理的数值。
管理编辑器的几何图形是委托的责任。当编辑器被创建和视图中数据项的大小或位置改变时,必须设置形状。幸运的是,视图在view option对象内部提供了所有必要的图形信息。
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
editor->setGeometry(option.rect);
}
在这个例子中,我们仅仅使用了视图选项提供的矩形的图形信息。一个用多个元素来渲染数据项的委托并不直接使用矩形信息。It would position the editor in relation to the other elements in the item.
编辑之后,委托应该给其他组件提供一些关于编辑处理流程的结果的提示,和一些帮助后续编辑操作的提示。通过用一个合适的提示来释放 closeEditor() 信号实现。这个信号被我们安装在Spin box上的默认事件过滤器处理。
可以通过调整Spin box的行为是它更人性化。在QItemDelegate提供的默认事件过滤器中,如果用户点击回车键来确认他们对spin box的选择,委托就会模型提交数值, 并且关闭spin box。我们可以通过安装我们自己的事件过滤器来改变这个行为,并且提供满足我们需求的编辑提示。例如,我们可以用 EditNextItem 提示释放 closeEditor()信号,来自动的开始编辑视图中的下一个数据项。
不要求使用事件过滤器的另一个方法是提供我们自己的编辑器窗口部件,为了方便起见,可以子例化 QSpinBox。这个替代的方法以编写更多的代码为代价,来提供我们对编辑器窗口部件更多的控制。如果你需要自定义一个标准QT编辑器窗口部件的行为,在委托中安装事件过滤器是更容易的。
委托并不是必须要释放这些提示的,但是那些没有释放提示信息的委托,很少会被集成到应用程序中,而且相对于那些通过释放提示信息来支持通用编辑行为的委托来说,更不实用。
视图类中使用的选择模型提供了一份基于模型/视图架构体系结构的选择的一般性说明。虽然对于现有提供的视图类来说,使用那些标准类来操作选择已经足够了,但是允许你创建专业的选择模型来满足你自己的模型和视图的要求。
视图中被选中数据项信息被存储在 QItemSelectionModel类中。这在一个单独的模型中为数据项维持模型索引,并且独立于任何视图。因为可能会有多个视图映射到同一模型,所以使视图间共享选择成为可能,允许应用程序同步的显示多个视图。
选择是由选择范围组成的。通过记录每组选中的数据项的起始模型索引来有效的维护大量选中数据项的信息。非连续的选择通过使用多个选择范围来描述。
选择被应用于选择模型所持有的一批模型索引。最新的选中项被称为current selection。选择的效果可以通过使用某些类型的选择指令被修改。这将在小节的后部分讨论。
在一个视图中,总有一个当前项和一个选中的数据项,两个独立的状态。一个数据项可以同时是当前项和选中项。视图的责任是保证随着键盘导航总有一个当前项,例如,请求一个当前项。
下表高亮强调了当前项与选中项的差别:
当前项 | 选中项 |
只能有一个当前项 | 可以有多个选中项 |
伴随着按键导航和鼠标点击,当前项被修改 | 当用户与数据项交互时,数据项的选中状态取决于一些预定义的模式,例如,单选,多选等等。 |
当前数据项将是可编辑的如果编辑快捷键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() 函数读取到。这个函数返回一个未排序的模型索引列表,只要我们知道模型索引属于哪一个模型,我们就可以使用迭代来给模型设置数据。
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()信号的槽函数,使用提供的信息来更新窗口的状态栏。
void MainWindow::changeCurrent(const QModelIndex ¤t,
const QModelIndex &previous)
{
statusBar()->showMessage(
tr("Moved from (%1,%2) to (%3,%4)")
.arg(previous.row()).arg(previous.column())
.arg(current.row()).arg(current.column()));
}
虽然用这些信号来监控用户做的选择很简单,但是我们也可以直接更新选择模型。
选择命令被一组由 QItemSelectionModel::SelectionFlag定义的选择标志提供。每一个选择标志都会告诉选择模型当select()函数被调用时,如何更新选中数据项的内部记录。最普遍使用的选择标志就是Select标志,这个标志指示选择模型来记录被选中的那些特定的数据项。Toggle标志导致选择模型把取消选定的数据项转换为选中的数据项,和把选中的数据项转换为未选择的。Deselect标志取消选定所有指定的数据项。
选择模型中的个别数据项,可以通过创建一个选择对象,并且把选择对象应用到选择模型上实现更新。在下面的代码中,我们把第二个选择对象应用到了上面的表格模型中,使用Toggle标志来转换选择状态。
QItemSelection toggleSelection;
topLeft = model->index(2, 1, QModelIndex());
bottomRight = model->index(7, 3, QModelIndex());
toggleSelection.select(topLeft, bottomRight);
selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);
操作的结果显示在了下面的表格视图中,提供了一个很方便的方式来可视化我们所做的操作。
默认情况下,选择命令只会操作模型索引指定的个别数据项。然而,这些用来描述选择命令的标志可以与一些额外的标志结合来实现对整行或列的改变。例如如果你以一个索引,但是Select和Rows标志的结合来调用select()函数,涉及到的这个数据项的一整行都会被选中。下面的例子证明了Rows和Columns标志的用法:
QItemSelection columnSelection;
topLeft = model->index(0, 1, QModelIndex());
bottomRight = model->index(0, 2, QModelIndex());
columnSelection.select(topLeft, bottomRight);
selectionModel->select(columnSelection,
QItemSelectionModel::Select | QItemSelectionModel::Columns);
QItemSelection rowSelection;
topLeft = model->index(0, 0, QModelIndex());
bottomRight = model->index(1, 0, QModelIndex());
rowSelection.select(topLeft, bottomRight);
selectionModel->select(rowSelection,
QItemSelectionModel::Select | QItemSelectionModel::Rows);
虽然只提供了四个索引给选择模型,但是使用了Columns和Rows选择标志以为着相关的两行和两列都会被选中。下面的图片展示了两个选择的结果。
在这个例子中执行的命令造成的效果都是累加的。也可以清除选择,或者以一个新的选择来替换当前选择。
通过Current标志与其它标志结合,把一个新的选择替换成当前选择。使用这个标志的命令指示选择模型用select()调用中指定的模型索引来替换当前的模型索引。通过Clear标志与其他选择标志的结合,在添加新的选择之前,清除所有已有的选择。这带来的效果就是清除选择模型的模型索引集合。
为了选择模型中的所有数据项,需要为包含所有数据项的每一个层级都创建一个选择对象。我们用一个父节点索引来获取左上和右下数据项的索引实现这个目的。
QModelIndex topLeft = model->index(0, 0, parent);
QModelIndex bottomRight = model->index(model->rowCount(parent)-1,
model->columnCount(parent)-1, parent);
用这些索引和这个模型来构造一个选择对象。在选择模型中对应的数据项会被选中。
QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);
这需要模型的所有层级都被执行。对于顶层的数据项,我们以通常的方式来定义父节点索引。
QModelIndex parent = QModelIndex();
对于多层级的模型,使用 hasChildren() 函数来确定给定的数据项是否是其他层级数据项的父节点。
模型/视图组件之间功能的分离,允许利用已存在的视图来创建新的模型。这种方式是我们可以把来自不同源的数据显示在标准用户界面,例如QListView, QTableView, 和 QTreeView。
QAbstractItemModel 提供一个足够灵活的接口来支持在层级结构中组织信息的数据源,允许按某种方式来实现数据的插入,移除,修改和排序。它也提供了拖拽的操作。
QAbstractListModel 和 QAbstractTableModel提供了更简单非层级结构的接口,更容易作为简单的列表和表格模型来使用。
在这一节中,我们创建一个简单的只读模型来探索模型/视图架构的基本概念。之后,我们会调整这个模型,一边用户可以修改数据项。
Simple Tree Model展示了一个更复杂的模型例子。
QAbstractItemModel 子例化的要求在Model Subclassing Reference详细讨论。
当为一个已经存在的数据结构创建模型时,考虑使用那种类型的模型来提供到数据的接口是很重要的。如果这个数据结构可以被表示为列表或者表格,你可以子例化QAbstractListModel 或 QAbstractTableModel,因为这些类提供了很多函数的默认实现。
然而,如果底层的数据结构只能被表示为层级结构,子例化 QAbstractItemModel就很必要。 Simple Tree Model采用了这种方法。
在这一节中,我们实现了一个基于字符串列表的简单模型。因此 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()函数的责任就是返回索引参数对应的项的数据。
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() 函数来给模型提供的数据提供相关的头部信息。
一个数据项可以有好几个角色,根据指定的角色来发出不同的数据。在我们的模型中,只有一个QDisplay角色,因此无论指定的角色是什么我们都会返回数据项的数据。然而我们可以在其他角色中重复使用我们提供给QDisplay角色的数据,例如视图可以使用 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。
从模型中删除行的函数也是很容易编写的。提供的position和行数指定了要从模型中删除的行数。我们忽略了父节点的索引来简化我们的实现,然后从字符串列表中移除对应的项。
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 类以垂直列表的形式来显示模型的数据项。对于一个字符串列表,视图也提供了一个默认的编辑器以便数据项可以操作。 We examine the possibilities made available by the standard view classes in View Classes.
Model Subclassing Reference更详细的讨论了QAbstractItemModel 子例化的要求,并且提供了一个为了使能不同模型中不同特性而必须重写的虚函数指南。
基于项的窗口部件拥有表示它们的作用的名字:QListWidget提供了一列数据项,QTreeWidget显示一个多层树结构,QTableWidget提供了一个数据项表。每一个类都继承了QAbstractItemView的行为,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);
列表中的每一项都可以显示一个文本标签和一个图标。可以通过改变被渲染文本的字体和颜色来提供一个自定义的数据项的外观。工具提示框,状态提示框,“what’s is this?”都很容易配置,确保这个列表被正确的集成到应用中。
newItem->setToolTip(toolTipText);
newItem->setStatusTip(toolTipText);
newItem->setWhatsThis(whatsThisText);
默认情况下,列表中的项,跟它创建的顺序是一样的。数据项可以根据 Qt::SortOrder 提供的条件来排序,产生一列以字符正序或逆序的数据项。
listWidget->sortItems(Qt::AscendingOrder);
listWidget->sortItems(Qt::DescendingOrder);
树形,或层级列表项被QTreeWidget 和 QTreeWidgetItem classes提供。在树形组件中的每一项都可以有它自己的子项,并且可以显示若干列信息。树形组件也像其它组件一样被构造:
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开始的。
对于每个易用的类都有许多基于项的公共特性,它们在每个类中的接口都是一样的。我们在之后的章节中,将以不同空间的例子来展示它们。查看 Model/View Classes可以获得每一个函数的更详细的用法。
有时候在一个项视图控件中,隐藏一个项比移除它更有用。上面提到的所有窗口部件中的项都可以被隐藏,然后在之后被再次显示。你可以通过isItemHidden() 函数来确定一个项是否被隐藏了,也可以通过setItemHidden()函数来隐藏一个项。
因为这个操作是基于项的,所以这些函数对于所有这三个简便的类都是有效的。
项被选择的方式是通过选择模型(QAbstractItemView::SelectionMode)来控制的。这个属性控制了是否用户可以选择一个或多个项,和在一个多项的选择中,选择是否必须是连续范围的。这个选择模型在上述三种窗口部件中的用法都是一样的。
窗口部件中的已选中项可以通过 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<QTreeWidgetItem *> found = treeWidget->findItems(
itemText, Qt::MatchWildcard);
foreach (item, found) {
treeWidget->setItemSelected(item, true);
// Show the item->text(0) for each item.
}
上述代码导致树形控件的项被选中,如果这些项的文本包含查询字符串的话。这些模式也可被用在列表和表格控件中。
模型/视图架构完全支持QT的拖拽功能。列表,表格,树中的项可以在视图内部拖放,数据也可以作为MIME-encoded的形式进行导入和导出。
标准视图自动支持内部的拖拽功能,在这种情况下,通过移动项来改变它们的显示顺序。默认情况下,这些视图是默认不启用拖拽功能的,因为它们是按照最简单最通用的目的配置的。为了允许项的拖放,需要使能某些视图属性,并且项本身也需要允许出现拖拽操作。
对于一个模型来说,允许从视图中导出项,而不允许数据导入它比完全使能拖拽操作的模型要求更低。
关于在新的模型中使能拖拽属性的信息请查看 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对象, just as in any other drag and drop operation。
下面代码显示了对应于一列索引的每项数据是如何被编码为纯文本的和如何存储在 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;
}
因为提供了一列模型索引给这个函数,所以同常在层级和非层级模型中使用这种方法已经是足够的。
注意,自定义的数据类型必须声明为meta objects,也必须为它们实现流操作符。详情请看 QMetaObject。
任何给定的模型处理被放置的数据的方式取决于它的类型(列表,表格,树)和它的内容展示给用户的方式。通常采取容纳被放置的数据的方式是最适合模型底层数据存储的。
不同类型的模型倾向于采取不同的方式处理被放置的数据。列表和表格仅提供了一个存储项数据的平面结构。结果就是,当数据被放置在一个视图中已存在的项上时,它们可能会插入一个新行或新列,或者它们可能会用提供的数据覆盖项的内容。树形模型通常能够把包含新数据的子项添加到它们的底层存储结构中,就用户关心的而言,and will therefore behave more predictably as far as the user is concerned.
通过一个模型的重写函数 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;
如果提供的数据不是纯文本形式,或者目的位置的列数是无效的,通过一个简单的字符串列表模型就可以表明操作失败。
被插入到模型中的数据将依据它是否被放置到一个已存在的项上而被区别对待。在这个简单的例子中,我们想要的是,允许把数据放置在已存在的项上,或者列表的第一项之前,或者列表的最后一项之后。
当一个放置操作发生时,父项对应的模型索引可能是有效的,表明放置操作发生着一个数据项上,或者也可能是无效的,表明放置操作发生在与模型顶层对应的视图中的某个地方。
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()函数执行自定义的比较操作,提供了自定义的排序行为。
模型子类化需要提供很多在QAbstractItemModel基类中定义虚函数的实现。需要被实现的函数的数量取决于模型的类型,是否它提供给视图一个列表,表格或者复杂的层级结构。继承自 QAbstractListModel 和 QAbstractTableModel的模型可以利用这些类提供的函数的默认实现。以树形结构形式显示数据项的模型必须提供 QAbstractItemModel中的很多虚函数的实现。
在一个模型子类中需要被实现的函数可以分为3组:
数据处理:所有的模型都需要实现能够让视图和委托查询模型的维度,检查数据项和获取数据的函数。
导航和创建索引:层级结构模型需要提供一些函数,视图通过调用这些函数来导航那些在它们上面显示的树形结构,并且为项获取模型索引。
拖放支持和MIME类型处理:模型继承一些函数,这些函数控制内部和外部的拖放操作的执行方式。这些函数允许数据项被描述为其他组件和应用能够理解的MIME类型。
对于模型提供的数据,模型可以提供很多不同的访问等级。它们可能是简单的只读组件,一些支持调整大小操作的模型和另一些允许项可编辑的模型。
为了提供对数据的只读访问,必须在模型子类中实现下列函数:
flag() | 被其它组件用来获取模型提供的每一个数据项的信息。在很多模型中,标志的组合应该包含 Qt::ItemIsEnabled和Qt::ItemIsSelectable。 |
data() | 用来向视图和委托提供数据。通常,模型只需为数据提供 Qt::DisplayRole和面向应用的用户角色,但是为数据提供Qt::ToolTipRole, Qt::AccessibleTextRole, 和 Qt::AccessibleDescriptionRole也是很好的做法。查看[Qt::ItemDataRole](http://doc.qt.io/qt-5/qt.html#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证明操作失败,使其他组件能够处理这些情况。
在调整大小的函数接口中调用这些函数而释放的信号,让相关的组件有机会在任何数据失效之前做一些操作。用begin和end函数封装的插入和移除操作也使模型能够正确管理持久的模型索引。
正常情况下,begin和end函数能够通知其他组件关于模型底层数据结构的改变。对于模型数据结构的更复杂的变化来说,也许涉及了内部的重新组织,或者排序数据,因此释放 layoutChanged()信号导致更新任何相关的视图也是很有必要的。
canFetchMore() 函数核对父项是否有更多可用的数据,相应的返回true或false。fetchMore() 基于指定的父项获取数据。但是这些函数可以被结合起来,例如,在数据库查询中,涉及增量数据填充QAbstractItemModel。我们实现 canFetchMore()函数表明是否可以过去更多数据,和 fetchMore() 函数按照要求填充模型。
另一个例子是动态填充树模型,当一个树模型中的分支被扩展时,我们重写 fetchMore() 函数。
如果你的 fetchMore() 重写实现中,向模型中添加了新行,你需要调用 beginInsertRows() 和 endInsertRows()函数。而且当 canFetchMore() 和 fetchMore() 的默认实现返回false或者什么都没做时需要重写这两个函数。
翻译自:http://doc.qt.io/qt-5/model-view-programming.html