Qt Model/View编程介绍

Qt中包含了一系列的项视图类,它们使用model/view的架构去管理数据之间的关系以及它们被展示给用户的方式。这种由这种架构引进的功能分离特性给了开发者很大的灵活性去自定义自己的展示方式,并且提供了一个编制的模型接口以使很多种的数据源都可以和现存的视图相结合。那么,今天我们就来简单的看下这种model/view范例,相关的概念,并简要描述一下项视图系统的架构。

模型/视图 架构

Model-View-Controller(MVC)是一种起源于Smalltalk编程语言的设计模式,主要被用来构建用户界面。对于这种设计模式,Gamma et al 写道:

         MVC由3中对象组成。模型(Model)是应用程序的数据,视图(View)是它的屏幕展示,控制器(Controller)则定义了用户界面响应用户输入的方式。在MVC之前,用户界面的设            计倾向于将这些东西杂揉到一起。MVC则通过对它们进行解耦来提高开发的灵活性和组件的复用性。

如果视图和控制器被组合在了一起,结果就是model/view架构了。这仍然区分了数据的存储方式和它被展示的方式,不过是基于相同的原则提供了一个更简单的框架。这种分离使我们有可能用多种不同的视图来展示同一种数据,并且可以在不改变底层数据结构的情况下实现新的视图类型。而为了灵活的处理用户输入,我们又引入了代理(Delegate)的概念。在model/view框架中引入代理的好处是允许数据项的渲染和编辑可以被自定义。所以,传统的MVC在Qt中就变成了MVD,其工作原理图如下:


其中,模型和数据源进行交互,为架构中的其他组件提供一个接口。当然,交互的本质取决于数据源的类型和模型被实现的方式;视图从模型中获得模型下标(model indexes),这些下标是对数据项的引用。通过为模型应用模型下标,视图就可以从数据源中获得数据项。在标志视图下,会有一个代理来渲染这些数据项。当数据项被编辑时,代理又会使用模型下标直接和模型进行交互。

通常情况下,model/view相关的类可以分为三组:模型,视图和代理。这些组件中的每一个都有相关的抽象类来定义,以此来提供一些通用的接口,并在某些情况下,提供一些特性的默认实现。这些抽象类可以被子类化以为其他组件提供完全的功能支持,也可以借此实现一些特定的组件。

模型,视图和代理彼此使用信号和槽进行通信:

  • 来自模型的信号会通知视图关于数据源中数据的改变
  • 来自视图的信号提供了用户和视图项发生交互的信息
  • 来自代理的信号被用于在编辑过程中告知模型和视图当前编辑器的状态。
模型
Qt中所有的模型都是基于QAbstractItemModel类的。这个类为视图和代理访问数据定义了一个接口。数据本身并不比存储在模型中;数据可以存储在一个数据结构中,或由其他类所提供的一个仓库中,文件中,数据库中,或其他应用程序组件。
QAbstractItemModel提供了一个对于数据的足够灵活的接口,可以应对各种形式的视图,比如表格,列表和树状列表。当时,当为列表或类表格数据结构实现新的模型时,QAbstractListModel和QAbstractTableModel类是一个很好的开始点,因为它们为大部分基础功能提供了默认实现。这些类中的每一个都可以被实例化来为特殊种类的列表和表格提供模型。
另外,Qt中还提供了一些现成的模型供我们使用:
  • QStringListModel:被用来存储一个QString项的简单列表。
  • QStanderItemModel:可以用来管理更复杂的树型数据结构,每一个数据项可以包含任意数据。
  • QFileSystemModel:提供本地文件系统中文件和目录的信息。
  • QSqlQueryModel,QSqlTabelModel和QSqlRelationalTableModel 被用作访问数据库的方便方法。
如果这些标准的模型不能满足我们实际的需求,我们还可以子类化QAbstractItemModel,QAbstractListModel和QAbstractTableModel来创建我们自己的模型。

视图
Qt中为不同种类的视图都提供了完整的实现:QListView显示一个数据项的列表,QTableView在一个表格中展示模型中的数据,QTreeView在一个有层级的列表中展示模型中的数据项。这些类都是基于QAbstractItemView抽象类。虽然这些类都是Qt提供的可以直接使用的类,但我们也可以子类化它们来实现一些自定义视图。

代理
QAbstractItemDelegate是模型/视图框架中的代理的基类。默认的代理实现由QStyledItemDelegate类提供,并且它被用作Qt的标准视图的代理。但是,对于视图中的项的绘制和编辑,QStyledItemDelegate 和 QItemDelegate是两个独立的可选方案。而它们之间的区别是QStyleItemDelegate类使用当前应用程序的样式来绘制每一项。因此,当实现自定义代理或和Qt的样式表配合使用时,我们推荐使用QStyledItemDelegate类作为我们的基类。

排序
在模型/视图中有两种排序的方式,至于选择哪一种取决于你的底层模型。
如果你的模型是可排序的,也就是说,如果它实现了QAbstractItemModel::sort()函数,那么QTableView和QTreeView都提供了相应的API允许你以编程的方式来对模型数据进行排序。此外,你还可以连接QHeaderView::sortIndicatorChanged()信号到QTableView::sortByColumn()槽函数上,或QTreeView::sortByColumn()槽函数上,来实现交互式的排序,即允许用户通过点击视图的表头还对模型数据进行排序。
如果你的模型没有需要的接口或者如果你想使用一个列表视图来展示你的数据,一个可选的方案是使用一个代理模型在视图展示数据之前对数据结构进行一个转换。关于这个方案,我们会在代理视图中详细讲解。

方便使用的类
其实,Qt库中还为我们提供了现成的控件类,这些类也是从Qt的标准视图类派生下来的,以供那些依赖模型视图的应用程序使用。但这些类一般不应该被子类化。
这些类包括 QListWidget,QTreeWidget和QTableWidget。
但这些类没有视图类灵活,也不能应用于任意的模型。所以,我们推荐你使用模型/视图的方式去处理基于项的数据.。除非你确实需要基于项的控件类。
如果你希望利用模型/视图这种方式的优点,又想使用基于项的控件类,可以考虑使用视图类,比如QListView,QTableView,QTreeView,结合QStandardItemModel。

下面我们就来详细看一看怎么在Qt中进行模型/视图编程。
Qt中的两种模型
QStandardItemModel和QFileSystemModel是Qt中提供的两种标准模型。QStandardItemModel是一个多用途的模型,可以被list,table和tree视图用来展示多种多样的数据结构。这个模型也持有数据项。QFileSystemModel模型主要是处理一个目录的人呢信息。因此,它不持有数据项本身,只是简单的展示本地文件系统中的文件和目录。
QFileSystemModel提供了一个现成的模型,并且经过简单的配置就可以使用现成的数据。使用这个模型,我们可以说明怎样为一个现成的视图设置模型,怎么使用模型下标操作模型中的数据。
QListView和QTreeView类是最适合使用QFileSystemModel模型的视图。下面的例子展示了在一个树型视图和一个列表视图中显示一个目录的内容。并且,这两个视图共享用户的选择操作,所以,每一个被选中的项会在两个视图中高亮显示。

其实现代码如下:
  int main(int argc, char *argv[])
  {
      QApplication app(argc, argv);
      QSplitter *splitter = new QSplitter;

      QFileSystemModel *model = new QFileSystemModel;
      model->setRootPath(QDir::currentPath());

      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();
  }
我们实例化了一个QFileSystemModel以便使用,并创建了相应的视图来展示一个目录下的内容。这是使用模型的简单的方式。该模型被初始化为使用一个文件系统的数据。调用setRootPath()告诉模型要将文件系统上的哪一个驱动器展示到视图中。我们在此创建了两个视图以便于用两种方式测试存储在模型中的数据。
由上面的代码可以看出,视图的创建方式和其他控件一样。要设置一个视图要显示的模型只需用要使用的模型作为参数调用视图类的setModel()。我们使用每一个视图的setRootIndex()来过滤模型提供的数据,在此我们为该函数传入了一个代表当前目录的的模型下标。上面所使用index()函数对应QFileSystemModel来说是唯一的;我们为它传入一个目录,它为我们返回一个模型下标。
至于这么处理视图中项的选择操作,我们会在后面的部分讲解。

模型类
在处理选择操作之前,我们先来看下模型/视图中的一些概念。
基本概念
在模型/视图架构中,模型为视图和代理提供了访问数据的标准接口。在Qt中,这些标准接口在QAbstractItemModel类中被定义。无论数据项存储在什么数据结构中,所有的QAbstractItemModel的子类都会把数据渲染为一个包含项目表的层级结构。视图使用这个约定的方式访问模型中的数据项,但它们也可以使用其他的方式来访问数据,并不局限于用和它们向用户展示数据的相同的方式。


同时,模型还会使用信号和槽向关联的视图通知数据的改变。

模型下标
为了确保数据的展示和数据的访问分开,便引入了模型下标的概念。通过模型可以获得的任何信息都是由一个模型下标表示的。视图和代理使用这些下标去请求要显示的数据项。
这样一来,只有模型需要知道怎么获取数据,并且被模型管理的数据的类型可以被定义的非常广泛。模型下标中包含一个指向创建它们的模型的指针,这在使用多个模型时可以避免混淆。
QAbstractItemModel *model = index.model();
模型下标提供了对信息片段的临时引用,可以被用来通过模型去获取或修改数据。因为模型随时都有可能重新组织它们的内部结构,模型下标可能会失效,所以不应该存储它们到一个变量中。如果需要对某种信息的长期引用,必须创建一个持久性的模型下标。这种下标会提供一个对模型信息的引用,并保持更新。临时模型下标由QModelIndex类表示,持久性的模型下标由QPersistentModelIndex类表示。
要得到一个对应于某个数据项的模型下标,则必须指明三个属性:一个行号,一个列号,一个该数据项父项的模型下标。下面,我们就来看一看这些属性。

行和列
在其最基本的形式下,一个模型可以被当做一个简单的表格来访问,此时每一个数据项都可以被它们所在的行和列来确定。但这并不意味着底层的数据是存储在一个数组结构中的;所使用的行号和列号只是一个约定,运行组件间彼此进行通信。我们可以通过指定一个数据项在模型中的行号和列号来得到任何数据项的信息,模型会向我们返回一个该数据项所对应的模型下标:
QModelIndex index = model->index(row, column, ...);
对于那些为简单、单级数据结构如列表和表格提供接口的模型来说,在访问一个数据项时,除了行号和列号,不需要提供其他的信息。但是,像上面的代码显示的,我们需要提供其他的信息才能获得一个模型下标。


上图表示了一个基本的表格模型,其中的每一项由一个行和列来确定。我们通过传递相对于该模型的行和列来得到一个引用某个数据项的模型下标。
  QModelIndex indexA = model->index(0, 0, QModelIndex());
  QModelIndex indexB = model->index(1, 1, QModelIndex());
  QModelIndex indexC = model->index(2, 1, QModelIndex());
对应模型中的顶层项来说,我们总是传入一个QModelIndex()来作为它们的父项。我们会在后面的部分继续讨论这个问题。

父元素项
当在一个表格或列表中使用数据时,类表格接口对于模型提供的数据来说是完美的;行列号系统精确的映射于视图显示数据的方式上。但是,树型视图这类的结构需要模型对其中的数据暴露更灵活的接口。结果,每一个项都可能是另一个表格项目的父,进而在树型视图中一个顶层项目可能包含另一个项目列表。
当请求一个模型项的下标时,我们必须提供该项的父项的相关信息。因为,在模型之外,引用一个项的唯一方式是通过一个模型下标,所以必须传入一个模型下标:
QModelIndex index = model->index(row, column, parent);



上图展示了一个树型视图,其中的每一项都由一个父项,一个行号,一个列号来确定。
项 "A" 和 "C" 是顶层兄弟项,可以用下面的方式获得:
  QModelIndex indexA = model->index(0, 0, QModelIndex());
  QModelIndex indexC = model->index(2, 1, QModelIndex());
项"A"有一系列的孩子。其中,"B"的模型下标可以用下面的方式获得:
QModelIndex indexB = model->index(1, 0, indexA);

项的角色
在模型中的每一项对其他组件来说都可能扮演多种角色,以允许不同种类的数据被应用于不同的情况。例如,Qt::DisplayRole是用来访问一个可以在视图中按文本显示的字符串。典型的,数据项包含了不同角色的数据,标准的角色由Qt::ItemDataRole类型来定义。
我们可以向模型请求某个项的数据,通过向它传递该项的模型下标,以及我们想要的数据类型的角色:
 QVariant value = model->data(index, role);
对于大部分常见用途的项数据,Qt::ItemDataRole类型的定义中均已包括。通过为每一种角色提供合适的项数据,模型可以为视图和代理提供提示该如果向用户展示该项。不同种类的视图均可以接受或忽略这些需要的信息。当然,也可以为特定的应用程序目的定义特定的角色。

自此,对于模型类的使用,我们可以进行一下总结:
  • 模型下标以独立于底层数据结构的方式向视图和代理提供了模型中的数据项的信息。
  • 项是由它们在模型中的行号和列号,以及它们的父项所指定的。
  • 模型下标在其他组件,如视图和代理,发出请求时被模型创建出来。
  • 当我们使用index() 函数请求一个下标时,若传递了一个有效的模型下标做为父项,那么返回的下标引用了一个在模型中位于父项下面的某项。这个下标引用着那个项的一个孩子。
  • 若使用index() 函数时,传入了一个无效的模型下标做为父项,那么返回的下标引用着模型中的一个顶层项
  • 角色区分了一个项所关联的不同数据。
使用模型下标
为了说明如何使用模型下标从一个模型中获取数据,我们初始化了一个QFileSystemModel,但不使用相关的视图,而是把文件和目录的名字显示到一个控件上。虽然,这不是一个使用模型的常规方式,但它确实说明了模型处理模型下标时的一些约定。
我们以下面这种方式来构建一个文件系统模型:
      QFileSystemModel *model = new QFileSystemModel;
      QModelIndex parentIndex = model->index(QDir::currentPath());
      int numRows = model->rowCount(parentIndex);
如上面的代码所示,我们创建了一个默认的QFileSystemModel,使用模型类的函数index()获取一个父下标,然后,我们使用模型类的rowCount()函数获取了该下标中的行数。
出于简单起见,我们只关心模型中第一列的数据。我们遍历每一行,获得每一行中第一项的模型下标,并读取出那一项所存储的数据。
      for (int row = 0; row < numRows; ++row) {
          QModelIndex index = model->index(row, 0, parentIndex);
          QString text = model->data(index, Qt::DisplayRole).toString();
          // Display the text in a widget.

      }
为了得到一个模型下标,我们指定了一个行号,一个列号(0代表第一个列),和一个代表我们想获得的所有数据的父项的模型下标。而每一项中存储的文本可以使用模型的data()函数获取到。我们通过指定一个模型下标和相应的DisplayRole已字符串的形式来获得某项存储的数据。
上面的例子虽小,但说明了从一个模型中获取数据的基本原则:
  • 一个模型的维度可以由rowCount() 和 columnCount()获得。这些函数通常需要指定一个父项的模型下标。
  • 模型下标被用来访问模型中的项。行号、列号和父项的模型下标是必须的,对于定位一个模型项来说。
  • 要访问模型中的顶层项目,可以使用QModelIndex()指定一个空的模型下标作为父项。
  • 项可以为不同的角色保存数据。为了获得特定角色的数据,需同时指定模型下标和角色。
视图类
概念
在模型/视图架构中,视图从模型中获取数据并将它们展示给用户。数据被展示的方式不必和数据在模型中的表示方式一样。数据被展示的方式甚至可以和存储数据的底层数据结构完全不同。
内容和展示的分离是通过使用一个QAbstractItemModel提供的标准模型接口,一个QAbstractItemView提供的标准接口和一种通用的模型下标来完成的。视图通常掌管着模型数据的所有布局。它们可以直接渲染每一个数据,或者使用代理去处理渲染和编辑。
和呈现数据一样,视图也处理数据间的导航,和与数据选择相关的方面。视图还实现基本的用户接口特性,例如上下文菜单和拖拽。视图也可以为模型项提供默认的编辑设备,或者与代理一起工作,提供一个自定义的编辑设备。
可以在没有模型的情况下构造一个视图,但若想让视图显示有用的信息,则必须为它设置一个模型。视图会跟踪用户使用selections选中的项,并且,selections可以每一个视图使用一个,也可以多个视图共享一个。
一些视图,例如QTableView和QTreeView,会把表头也看作是一个项。这些项也被Qt实现为一个独立的类,QHeatherView。表头通常也会访问包含它们的view所访问的模型。它们使用QAbstractItemModel::headerData()函数从模型中获取数据,并且,通常以一个label的形式来展示头信息。新的表头可以从QHeaderView类派生,来未视图提供更专业的信息。

使用现存的视图
和模型一样,Qt也提供了三种现成的视图类,它们可以用大部分用户熟悉的形式展示模型中的数据。QListView可以把来自模型的数据展示为一个简单的列表,或者一种典型的图标视图。QTreeView可以把来自模型的数据展示为一个有层级的列表,允许以一种紧凑的方式展示深层嵌套的数据结构。QTableView可以把来自模型的数据展示为一个表格。如下单所示:

上图显示的是标准视图的默认行为,这已经足够大部分应用程序使用的了。它们提供了基本的编辑设备,并且可以被自定义去适应更特化的用户界面的需求。

使用模型
  int main(int argc, char *argv[])
  {
      QApplication app(argc, argv);

  QStringList numbers;
  numbers << "One" << "Two" << "Three" << "Four" << "Five";

  QAbstractItemModel *model = new StringListModel(numbers);

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

      view->show();
      return app.exec();
  }
如上面代码所示,我们先创建了一个字符串列表模型(自定义),再为它初始化一些数据,然后创建一个视图显示这个模型的内容。
注意,StringListModel被声明为QAbstractItemModel。这允许我们对这个模型应用抽象的接口,并可以确保当我们用一个不同的模型来替换字符串列表模型时,我们的代码任然能够工作。
由QListView提供的列表视图足够展示这个字符串列表了。该视图会渲染这个模型中的内容,通过这个模型的接口访问数据。当用户尝试去编辑一个项时,该视图会使用默认的代理会用户提供一个编辑控件。

上图说明了QListView是如何展示这个字符串列表模型的。另外,因为该模型时可编辑的,所以,视图自动的使用默认代理使用列表中的每一项都可编辑。

为多个视图使用一个模型
在同一个模型上应用多个视图是很简单的,只需在每个视图上调用setModel()即可。代码如下:
      QTableView *firstTableView = new QTableView;
      QTableView *secondTableView = new QTableView;

      firstTableView->setModel(model);
      secondTableView->setModel(model);
在模型/视图中,信号和槽的使用意味着模型的改变可以被传播到所有相关的视图上,以此来确保无论使用哪个视图,我们总能访问到相同的数据。

上图说明了两个不的视如果作用在同一个模型上,并且,每一都包含了一些了选中的项。虽然,两个视图的数据是来自同一个模型,但每一个视图都包含了它自己的内部选择模型。这在很多情况下都是有用的。

处理元素的选中
视图处理选中的机制是由QItemSelectionModel类提供的。所有标准的视图在默认情况下都会创建它们自己的选中模型,并与它们进行正常的交互。我们可以使用selectionModel()函数获得一个视图当前正在使用的选中模型,也可以使用setSelectionModel()为视图重新设置一个选中模型。当我们想要在同一个模型上提供多个一致的视图时,这种控制视图使用的选中模型的能力是很有用的。
通常情况下,除非你正在子类化一个模型或视图,否则你不必直接操作selections的内容。但是,如果需要的话,我们可以访问选中模型的接口。

在视图间共享选中模型
经过默认情况下,视图提供它们自己的选中模型对我们来说是方便的,但当我们在一个模型上使用多个视图时,经常希望模型的数据和用户的选中在所有的视图中是一样的。既然视图类允许它们内部的选中模型被修改,我们就可以使用下面这行代码来在两个视图间共享选中模型:
secondTableView->setSelectionModel(firstTableView->selectionModel());
第二个视图的选中模型被设置为第一个视图的选中模型。此时,两个视图就工作与同一个选中模型了。它们会在数据和选择操作上保存一致。效果如下:

在上面的例子中,我们使用了两个同样类型的视图和同一个模型数据。但是,如果使用两种不同类型的视图,那么这些选中的项在两个视图将会得到不同的展现;例如,在表格视图中一系列连续的选中项,在一个树型视图可能被展示为了一系列的高亮选项的片段。

代理类
概念
不像Model-View-Controller模式,model/view模式不包含一个完全独立的组件来用于和用户进行交互。通常情况下,由视图负责模型数据的展示和处理用户的输入。为了使这种处理用户输入的方式更灵活,这些交互就由代理来完成。这些组件提供了输入的能力,同时也负责在视图中渲染各自的模型项。控制代理的标准接口在QAbstractiItemDelegate类中定义。
代理将可以自己渲染它们的内容通过实现paint()函数和sizeHint()函数。但是,简单的基于控件的代理可以继承QItemDelegate而不是QAbstarctItemDelegate类来实现,这可以享受到这些函数的默认实现。
代理所使用的编辑器可以通过让控件去掌管编辑过程来实现,也可以由代理直接响应事件来实现。

使用现存的代理
Qt提供的标准视图使用QItemDelegate类的实例来提供编辑设备。代理接口的这种默认实现会以一种常规的样式来渲染视图中每一个模型项。包括:QListView,QTableView和QTreeView。
所有标准的角色都由标准视图使用的默认代理来处理。每一个视图使用的代理可以由itemDelegate()函数返回。而setItemDelegate()函数为标准视图安装一个自定义的代理,并且,为自定义的视图设置代理也需要调用这个函数。

实现一个简单的代理
下面我们使用QSpinBox来使用一个简单的代理,主要用于显示整数的模型上。虽然我们创建的是基于整数的表格模型,但我们可以很容易的使用QStandardModel来替换,因为自定义的代理控制着数据的入口。我们创建一个表格视图来展示模型的内容,并使用自定义的代理来提供编辑功能。效果如下图:


此处,我们选择从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;
  }
注意,我们不需要保存执行该控件的指针,因为视图会负责在不需要它时将其释放。
我们在编辑器上安装代理默认的事件过滤器,类确保它提供用户期望的标准的编辑快捷键。也可以向编辑器添加额外的快捷键来提供更复杂的行为。
视图会负责调用我们定义的函数来正确的设置编辑器的数据和尺寸。我们可以基于视图提供的不同的下标创建出不同的编辑器。例如,如果我们有一列是整数,还一列是字符串,那么我们可以创建一个QSpinBox或者一个QLineEdit,取决于当前编辑的是哪一列。
代理同时必须提供一个函数将模型中的数据拷贝到编辑器。在这个例子中,我们读取模型中display role的数据,并相应地将它设置到spinbox。代码如下:
  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);
  }
在这个例子中,我们知道编辑器控件是一个spix box,但是我们可能为模型中不同的数据类型提供了不同的代理,所以,我们需要先将控件转化成正确的类型才能访问它的成员函数。

提交数据到模型
当用户在spin box中编辑结束,视图会要请求代理将编辑后的值存储到模型中,通过调用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);
  }

既然是视图帮代理管理着编辑器控件,所以我们只需要使用编辑器提供的值去更新模型即可。在这种情况下,我们要确保spin box 是最新的,并使用它包含的值通过指定的下标来更新模型。
标准的QItemDelegate类会在编辑结束后通过发送closeEditor()信号的方式来通知视图。视图就会确保编辑器控件被关闭和销毁。但在这个例子中,我们只提供了简单的编辑功能,所以我们不需要发送这个信号。
在数据上的所有操作都是通过QAbstractItemModel类的接口来实现的。这使代理在很大程度上独立于其操作的数据类型,但为了使用特定类型的编辑器控件还是要做一个假定的。在这个例子中,我们假定模型总是包含整形数据,但我们仍然能将该代理用于不同类型的模型上,因为QVariant类可以为未知的数据提供合理的默认值。

更新编辑器的尺寸
管理编辑器的尺寸就是代理的责任了。当编辑器被创建时必须设置它的尺寸,当编辑器的尺寸或位置发生变化时也是如此。幸运的是,视图在一个view option对象中提供了所有必须的尺寸信息。代码如下:
  void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
      const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
  {
      editor->setGeometry(option.rect);
  }
在这个例子中,我们仅仅使用了view option 对象在item rectangle 中提供的尺寸信息。使用多种元素渲染数据项的代理可能不直接使用item rectangle。它可能会相对于该数据项中其他的元素来安置这个编辑器。

编辑提示
编辑结束之后,代理应该向其他组件提供一些关于编辑结果的示意,并且还要为协助后续的编辑操作提供一些示意。这些操作可以通过发送closeEditor()信号,并携带一个合适的提示参数来完成。这会被我们在构造spin box时为其安装的默认事件处理器来处理。
spin box的行为可以被调整的对用户来说更加友好。在QItemDelegate提供的默认事件处理器中,如果用户在spin box中敲击了回车键来确定他们的选择,代理对象就会将选择的值提交到模型并关闭spin box。我们可以通过在spin box上安装我们自己的事件过滤器,提供我们需要的合适的编辑提示,例如,我们可以在发送closeEditor信号时携带一个EditNextItem 提示,来实现自动编辑视图中的下一项的功能。
另一个不需要使用事件过滤器的方法是提供我们自己的编辑控件,比如子类化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 = model->index(0, 0, QModelIndex());
      QModelIndex bottomRight = model->index(5, 2, QModelIndex());

为了在模型中选中这些项,并且在表格视图中看到相应的变化,我们需要构建一个选择对象,在把它应用到选择模型上:
      QItemSelection selection(topLeft, bottomRight);
      selectionModel->select(selection, QItemSelectionModel::Select);

该选集被应用到选择模型,通过一个预定义的选集标志。在上面这种情况下,所使用的的标志导致我们所传入的选集对象中的模型项被包含进了选择模型,无论他们先前是什么状态。结果显示如下图:


这些项的选集可以使用多种预定义好的选集标志进行修改。而这些操作所产生的选集的可能会有一个复合结构,但它仍可以被选中模型高效的展示。


读取选集状态

存储在选择模型中的模型下标可以使用selectedIndexed()方法读取。返回的是一个无序的列表,其中包含了我们可以迭代的模型下标,只要我们知道它们属于哪个模型:

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

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

选择模型也是通过发射信号来表现选择的变化。这可以通知其他组件有关于模型中整个选集和当前有焦点的项的变化。我们可以连接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()函数,但传入的选择命令是Select和Rows的组合,那么包含该行的整行都被选中。下面的代码展示了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);


虽然只向选择模型提供了4个下标,但使用了Columns和Rows标志,意味着2行和2列被选中。运行结果如下:



在上面例子模型中展示的命令都涉及到了在模型中累加一个选集中的模型项。当然,我们也可以清空一个选集,或用一个新的选集来替代当前的选集。要用一个新的选集替换当前的选集,可以在选择标志中组合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类为更简单的非层级数据结构提供了接口支持,对于简单的列表和表格模型这也是一个更容易使用的开始点。

下面,我们先创建一个简单的只读模型来展示基本的模型/视图架构的惯例。在后面,我们会改变这个简单模型,使它的项可以被用户修改。

设计一个模型

当为现存的数据结构创建一个新模型时,考虑哪种模型应该用来为数据提供接口是很重要的。如果该数据结构可以被展示为一个列表或表格,那么可以子类化QAbstractListModel 或者 QAbstractTableModel,因为这些类为很多操作提供了默认实现。

但是,如果底层的数据结构只能被展示为一个层级的树形结构,那就必须子类化QAbstractItemModel。

在这里,我们基于字符串列表实现一个简单的模型,所以QAbstractListModel是很好的一个基类选择。

其实,无论底层的数据结构是什么样的,在一个特化的模型中,为了更自然的访问底层数据结构,补充实现一些标准的QAbstractItemModel API总是一个好主意。这会让模型的填充更容易,也能使其他常规的模型/视图组件使用标准的API与它交互。下面自定义的模型出于目的提供了一个构造函数。

一个只读的模型

这儿实现的模型是一个简单的,非层级的,只读的,基于QStringListModel的模型。它有一个QStringList作为它的内部数据结构,只实现了能让模型运行的必须的函数。为了使实现更简单,我们子类化QAbstractListModel。当实现一个模型时,要注意的是QAbstractItemModel并不存储数据本身,它仅仅提供一个视图访问数据的接口。对于一个最小化的只读模型来说,我们只需要实现很少的几个函数,因为大部分接口都有了默认的实现。该类的声明如下:

  class StringListModel : public QAbstractListModel
  {
      Q_OBJECT

  public:
      StringListModel(const QStringList &strings, QObject *parent = 0)
          : QAbstractListModel(parent), stringList(strings) {}

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

  private:
      QStringList stringList;
  };

除了模型的构造函数外,我们只需要实现两个函数:rowCount()返回模型中数据的行数,data()返回模型中相对于特定模型下标的项。

行为良好的模型,还应该实现headerData()函数,来为树形或表格视图提供一些显示在头部的信息。

注意,这是一个非层级模型,所以我们不必关心父子关系。如果我们的模型是层级模型,可能还要实现index()和parent()函数。

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


模型的维度

我们想要让模型的行数和字符串链表中的项数一样。所以,我们如下实现rowCount()函数:

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

因为该模型是非层级的,所以我们可以安全的忽略父项的模型下标。默认情况下,从QAbstractListModel类派生的模型只包含一列,所以我们也没必要实现columnCount()函数。


模型头信息和数据的获取

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

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

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

      if (role == Qt::DisplayRole)
          return stringList.at(index.row());
      else
          return QVariant();
  }
我们只是在index有效,行数在有效范围内,请求的角色是模型所支持的情况下返回一个有效的QVariant类型的变量。

还有一些视图,例如QTreeView和QTableView,同数据一道还可以显示一些头部信息。如果我们的模型被显示在一个具有头信息的视图中,并我们想在表头显示行号和列号。我们可以实现headerData()方法来提供这些信息:

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

      if (orientation == Qt::Horizontal)
          return QString("Column %1").arg(section);
      else
          return QString("Row %1").arg(section);
  }
再一次,我们在角色是模型所支持的情况下,返回一个有效的QVariant变量。并且,在返回具体的数据时我们也考虑了头部的方向问题。

不是所有的视图都显示头部信息,还有一些视图会隐藏它们。尽管如此,我们还是推荐你去实现headerData()函数,来为模型提供的数据提供一些相关的描述信息。

一个模型项可以有多个角色,对于指定的不同的角色要返回不同的数据。在我们自定义的模型中只有一种角色,DisplayRole,所以我们为每一项返回数据而不关心其指定的角色。但是,我们还是可以将我们为DisplayRole提供的数据用于其他角色的,比如ToolTipRole,这样,视图在一个提供框中显示一些关于当前项的信息。


可编辑的模型

上面的只读模型可以向用户展示一些简单的信息,但是,对于很多应用程序来说,一个可编辑的列表模型是更有用的。我们可以通过修改为只读模型实现的data()函数来使模型中的项可以被编辑,当然,还有提供另外两个函数:flags()和setData()。我们先将下面的函数添加到类声明中:

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


代理会在创建编辑器之前检测一个模型项是否可编辑。模型必须让代理只读它的项是可编辑的。我们可以通过为模型中的每一项返回一个正确的标志来实现这个功能;在这种情况下,我们使能所有的模型项,并且使它们可以被选择和编辑:

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

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

我们不需要知道代理具体是怎么处理编辑过程的。我们只需给代理提供一个方式将数据设置会模型。这是通过setData()函数完成的:

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

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


在这个模型中,我们使用函数提供的值来替换字符串列表中对应位置的值。但是,我们必须确保下标是有效的,项的类型是正确的,角色是受支持的。按照惯例,我们强调角色是EditRole,因为这是标准代理支持的角色。但对于boolean值来说,你可以使用Qt::CheckStateRole角色和设置Qt::ItemIsUserCheckable标志;然后,提供一个复选框来编辑数据。因为此处模型中底层数据是同一个角色,所以这些细节使它更容易与标准组件集成。

当数据被给设置后,模型必须让视图知道一些数据已经改变了。我们通过发射dataChanged()信号完成这个功能。因为此处只有一项发生了变化,所以信号携带的参数被限制为一个模型下标。

下面,来修改data()函数,加入Qt::EditRole的判断:

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

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

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

传入和删除行

模型中的行列数是可以被改变的。但在上面我们自定义的字符串模型中,只有一列,所以我们只要实现插入和删除行的函数即可。在类声明中加入下面这些函数的声明:

      bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex());
      bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex());
因为行在这个模型中就对应于列表中的字符串,insertRows()函数就是想列表中指定位置前面插入一些空字符串。插入的字符串数量等于指定的行的数量。

父下标通常被用于决定这些行被插入到模型中哪个地方。在这个例子中,我们只有一个顶层列表,所以,我们只需将空字符串插入列表即可。

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

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

      endInsertRows();
      return true;
  }

模型首先调用beginInsertRows()函数来通知其他组件行数将要发生变化。这个函数指定将要插入的第一行和最后一行的行号,以及它们的父下标。修改字符串列表之后,又调用endInsertRows()来完成操作和通知其他组件模型的维度发生了变化,返回true指示成功。

同样,删除行的实现方式也类似:

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

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

      endRemoveRows();
      return true;
  }


Qt中视图控件

Qt的基于项的控件的名字就反应它们各自的作用:QListWidget提供了一个项的列表,QTreeWidget展示了一个多级的树形结构,QTableWidget提供了项的表格展示。每一个类都继承了QAbstractItemView类的行为,该类为项的选择和头信息的管理提供了公共行为。

列表控件

单层级的项列表通常使用QListWidget和一系列的QListWidgetItem来展示。List Widget的构建和其他控件一样:

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

列表控件中的每一项可以显示一个文件标签和一个图标。文本的颜色和字体可以改变,从而为每一项提供一个自定义的外观。Tooltips,status tips和“What's This?”帮助也是很容易配置的:

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

默认情况下,列表中的项是按它们被插入的顺序放置的。项列表可以根据Qt::SortOrder中给出的标准排序,从而产生一个按字母正序或逆序放置的列表:

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

树形控件

树或层级的列表是由QTreeWidget和QTreeWidgetItem类来提供的。树形控件中的每一项都可以有它们自己的孩子,并可以显示多列的信息。树形控件的创建和其他控件一样:

QTreeWidget *treeWidget = new QTreeWidget(this);
在项被插入树形控件之前,必须先设置列数。例如,我们可以定义两列,并为每一列提供一个头信息:

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


为每一列提供信息的最简单的办法就是使用一个字符串列表。对应更复杂的头部来说,你可以创建一个树项,按你希望的方式装饰它,然后把它作为树控件的头。

树形控件的顶层项以树形控件为父控件进行创建。它们可以以任意的顺序被插入,或者你可以通过在创建每一个树项时指定父项的方式让它们按一定的顺序排列:

      QTreeWidgetItem *cities = new QTreeWidgetItem(treeWidget);
      cities->setText(0, tr("Cities"));
      QTreeWidgetItem *osloItem = new QTreeWidgetItem(cities);
      osloItem->setText(0, tr("Oslo"));
      osloItem->setText(1, tr("Yes"));

      QTreeWidgetItem *planets = new QTreeWidgetItem(treeWidget, cities);


树形控件处理顶层项和其他更深层的项有一点不同。项可以从树的顶层被删除通过调用树形控件的takeTopLevelItem(),但更底层的项是通过调用它们父项的takeChild()函数来删除的。顶层的项是用过insertTopLevelItem()函数插入的,更底层的项是通过它们的父项的insertChild()来插入的。

我们可以很容易的删除树形控件中的顶层项和底层项。只需要判断一下这个项是否是顶层项即可,而这个信息可以由每一个项的parent()函数提供。例如,下面的代码从树控件中删除一项:

      QTreeWidgetItem *parent = currentItem->parent();
      int index;

      if (parent) {
          index = parent->indexOfChild(treeWidget->currentItem());
          delete parent->takeChild(index);
      } else {
          index = treeWidget->indexOfTopLevelItem(treeWidget->currentItem());
          delete treeWidget->takeTopLevelItem(index);
      }
在树形控件中的某个位置插入一项也是如此:

      QTreeWidgetItem *parent = currentItem->parent();
      QTreeWidgetItem *newItem;
      if (parent)
          newItem = new QTreeWidgetItem(parent, treeWidget->currentItem());
      else
          newItem = new QTreeWidgetItem(treeWidget, treeWidget->currentItem());

表格控件

表格控件是由QTableWidget和QTableWidgetItem类提供的。这些类提供了一个滚动的保护头信息和项的表格控件。
表格控件可以使用一个行数和列数作为参数来创建:

      QTableWidget *tableWidget;
      tableWidget = new QTableWidget(12, 3, this);

表格中项的创建如下:

      QTableWidgetItem *newItem = new QTableWidgetItem(tr("%1").arg(
          pow(row, column+1)));
      tableWidget->setItem(row, column, newItem);


水平的和垂直的头也可以在表格外创建,然后把它们用作头部信息:

      QTableWidgetItem *valuesHeaderItem = new QTableWidgetItem(tr("Values"));
      tableWidget->setHorizontalHeaderItem(0, valuesHeaderItem);


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


公共特性

在这些基于项的控件中有一些通用的基于项的特性可以使用同一个接口来获得。我们下面来具体看一看。

隐藏项

有时候在一个视图控件中隐藏一个项比把它们从控件删除要好的多。上面提到的控件的项都可以被隐藏和重新显示。可以使用isItemHidden()函数来判断一个项是否被隐藏了;可以使用setItemHidden()来隐藏一个项。

因为这个操作是基于项的,所以上面提供的三个类都有这个接口。

选择

项被选中的方式由控件的选择模式控制(QAbstractItemView;:SelectionMode)。这个属性控制着用户是否可以选中一个或多个项,以及在多项选择中,被选中的项是否必须连续。上面提到的三个类有同样的选择模式。

控件中被选中的项可以通过selectedItems()函数获取到,其返回一个可被迭代的包含相关项的列表。例如,我们可以使用下面的代码来计算选中项的数据的总和:

      QList<QTableWidgetItem *> 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()来完成这个功能。

控件中的行通过它们包含的文本被搜索到,当然还要在调用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数据来导入或导出。

标准视图内部自动的支持拖放,它们的项可以移动来改变它们被显示的顺序。默认情况下,这些视图的拖放功能未被启用,因为它们被配置为最简单,最通用的控件。为了使项可以被拖动,视图的某些属性需要被启用,并且项本身也必须支持拖动。


视图控件中的拖放

默认情况下,QListWidget,QTableWidget和QTreeWidget所使用的每一个项的类型被配置为使用不同的标志。例如,每一个QListWidgetItem或QTreeWidgetItem初始条件下是使能的,可以选中的,可以选择的,也可以做为一个拖放操作的源;每一个QTableWidgetItem是可以被编辑的,可以作为拖放操作的目标的。

尽管所有的标准项具有一个或两个标志被设置用于拖放,你通常还需要设置视图本身的多个属性,以此来利用内建的拖放支持:

  • 使项可以拖动,设置视图的dragEnabled属性。
  • 使用户可以在视图上放置一个内部或外部的项,设置视图的viewport()的acceptDrops属性为真。
  • 为了给用户展示当前被拖动的项将被放在哪里,设置视图的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中的任意组合,但模型必须支持所提供的所有组合。例如,为了在模型中正确的使用QtMoveAction标志,模型必须提供QAbstractItemModel::remveRows()方法,无论是直接实现还是间接的从基类继承。

启用模型项的拖放
模型通过实现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::mimeType()函数类声明它能为模型项提供的MIME类型。例如,如果一个模型只支持纯文本,可以提供如下的代码实现:
  QStringList DragDropListModel::mimeTypes() const
  {
      QStringList types;
      types << "application/vnd.text.list";
      return types;
  }
该模型还必须提供广告格式的代码编码数据。这是通过实现QAbstractItemModel::mimeData()函数完成的,该函数会返回一个QMimeData对象。
下面的代码展示了给定下标处的每一个数据项,怎么被编码成纯文本数据存储在QMimeData对象中:
  QMimeData *DragDropListModel::mimeData(const QModelIndexList &indexes) const
  {
      QMimeData *mimeData = new QMimeData();
      QByteArray encodedData;

      QDataStream stream(&encodedData, QIODevice::WriteOnly);

      foreach (const QModelIndex &index, indexes) {
          if (index.isValid()) {
              QString text = data(index, Qt::DisplayRole).toString();
              stream << text;
          }
      }

      mimeData->setData("application/vnd.text.list", encodedData);
      return mimeData;
  }
因为该函数接受一个下标列表,所以该函数可以应用于层级的和非层级的模型中。
但要注意,对于自定义的数据类型来说,必须将其声明为 meta object,并为它们定义流操作符。

项模型中插入拖放的数据
任何模型处理拖放数据的方式都取决于它自身的类型(列表,表格,树形)和内容被展示给用户的方式。总的来说,应该采取一种能基本适应模型底层数据的方法。
不同的模型处理拖放数据的方式也不同。列表和表格只是提供了一个普通的结构来存储数据项。结果,当数据被拖放到视图中时,它们会插入一个新行或新列,也有可能会使用 提供的数据重写模型中某项的内容。树形模型经常能够向它的底层数据存储中插入新的子项,所以,它的行为经常是不可预测的。
拖放的数据经常是由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 *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()函数。

自此,我们就大致了解了Qt中模型/视图框架的使用及基本原理。至于其他的信息,特别是模型/视图的自定义,还需要大家多练习,多参看Qt的帮助文档。











 







©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页