应用程序中往往要存储大量的数据,并对他们进行处理,然后可以通过各种形式显示给用户,用户需要时还可以对数据进行编辑。Qt中的模型/视图架构就是用来实现大量数据的存储、处理及其显示的。
模 型 / 视 图 框 架
MVC(Model-View-Controller)是一种起源于Smalltalk的设计模式,经常用于创建用户界面。MVC包含了3个组件,模型(Model)是应用对象,用来表示数据;视图(View)是模型的用户界面,用来显示数据;控制(controller),定义了用户界面对用户输入的反应方式。在MVC之前,用户界面都是将这3种组件集合在一起,MVC将它们分开,从而提高了灵活性和重用性。
如果将视图和控制两种组件结合起来,就形成了模型/视图框架。这同样将数据的存储和数据向用户的展示进行了分离,但提供了更为简单的框架。数据和界面进行分离。使得相同的数据在多个不同的视图中进行显示成为可能,而且还可以创建新的视图,而不需要改变底层的数据框架。为了对用户输入进行灵活处理,还引入了委托(Delegate)的概念,使用它可以定制数据的渲染和编辑方式。模型/视图的整体框架如下图所示。其中,模型与数据源进行通信,为架构中的其他组件提供了接口,视图从模型中获取模型索引,模型索引用来表示数据项。在标准的视图中,委托渲染数据项,编辑项目时,委托使用模型索引直接与模型进行通信。
组 成 部 分
大体上,模型/视图架构中的众多类可以分为3组:模型、视图和委托。其中,每一个组件都使用了一个抽象基类来定义,提供了一些通用接口和一些功能的默认实现,模型、视图、委托之间使用信号和槽来实现通信:
>当数据源的数据发生改变时,模型发出信号告知视图;
>当用户与显示的项目交互时,视图发出信号来提供交互信息;
>当编辑项目时,委托发出信号,告知模型和视图编辑器的状态。
1.模 型
所有的模型都基于QAbstractItemModel类,这个类定义了一个接口,可以供视图和委托来访问数据。数据本身并不一定要存储在模型中,也可以存储在一个数据结构、一个独立的类、文件、数据库或者应用程序的其他一些组件中。
QAbstractItemModel为数据提供了一个十分灵活的接口来处理各种视图,这些视图可以将数据表现为表格、列表和树等形式。然而,要实现一个新的模型时,如果它基于列表或者表格的数据结构,那么可以使用QAbstractListModel和QAbstractTableModel类,因为它们为一些常见的功能提供了默认的实现。这些类都可以被子类化来提供模型,从而支持特殊类型的列表和表格。
Qt提供了一些现成的模型来处理数据项:
>QStringListModel用来存储一个简单的QString项目列表;
>QStandardItemModel管理复杂的树型结构数据项,每一个数据项可以包含任意的数据;
>QFileSystemModel提供了本地文件系统中文件和目录的信息;
>QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel用来访问数据库。
如果Qt提供的这些标准模型无法满足需要,还可以子类化QAbstractItemModel、QAbstractListModel或者QAbstractTableModel来创建自定义的模型。
2.视 图
Qt提供了几种不同类型的视图:QListView将数据项显示为一个列表,QTableView将模型中的数据显示在一个表格中,QTreeView将模型的数据项显示在具有层次的列表中。这些类都基于QAbstractItemView抽象基类,这些类可以直接使用,还可以被子类化来提供定制的视图。
3.委 托
在模型/视图框架中,QAbstractItemDelegate是委托的抽象基类,默认的委托实现由QStyledItemDelegate类提供,这也被叫做Qt标准视图的默认委托。然而,QStyleItemDelegate和QItemDelegate是相互独立的。只能选择其一来为视图中的项目绘制和提供编辑器。它们的主要不同就是,QStyledItemDelegate使用当前的样式来绘制项目,因此,当要实现自定义的委托要和Qt样式表一起应用时,建议使用QStyleItemDelegate作为基类。
模 型 类
在模型/视图架构中,模型提供了一个标准的接口供视图和委托来访问数据。在Qt中,这个标准的接口使用QAbstractItemModel类来定义。无论数据项是怎么存储在何种底层数据结构中,QAbstractItemModel的子类都会以层次结构来表示数据,这个结构中包含了数据项表。视图按照这种约定访问模型中的数据项,但是这不会影响数据的显示,视图可以使用任何形式将数据显示出来。当模型中的数据发生了变化时,模型会通过信号和槽机制告知与其相关联的视图。
基本概念
常用的3种模型分别是列表模型、表格模型和树模型,它们的示意图如下所示:
1.模 型 索 引
为了确保数据的表示与数据的获取相分离,Qt引入了模型索引的概念。每一块可以通过模型获取的数据都使用一个模型索引来表示,视图和委托都使用这些索引来请求数据项并显示。这样,只有模型需要知道怎样获取数据,被模型管理的数据类型可以广泛地被定义。模型索引包含一个指针,指向创建它们的模型,使用多个模型时可以避免混淆。
模型索引由QModelIndex类提供,它是对一块数据的临时引用,可以用来检索或修改模型中的数据。因为模型可以随时对内部的结构进行重新组织,这样模型索引可能失效,所以不需要也不应该存储模型索引。如果需要对一块数据进行长时间的引用,则必须使用QPersistentModelIndex创建模型索引。如果要获取一个数据项的模型索引,则必须指定模型的3个属性:行号、列号和父项的模型索引。
2.行 和 列
在最基本的形式中,一个模型可以通过把它看作一个简单的表格来访问,这时数据项可以使用行号和列号来定位。但这并不意味着底层的数据项是存储在数据结构中的,使用行号和列号只是一种约定,以确保各组件间可以相互通信。
行号和列号都是从0开始的,其中列表模型和表格模型的所有数据项都是以根项为父项的,这些数据项都可以被称为顶层数据项;在获取这些数据项的索引时,父项的数据项可以用QModelIndex()表示。
3. 父 项
前面讲述的类似于表格的接口对于在使用表格或者列表时是非常理想的,但是,像树视图一样的结构需要模型提供一个更加灵活的接口,因为每一个数据项都可能成为其他数据项表格的父项,一个树视图的顶层数据项也可能包含其他的数据项列表。当为模型项请求一个索引时,就必须提供该数据项父项的一些信息。所以在树模型中,如果一个数据项不是顶层数据项,那么就要指定它的父项索引。
4.项 角 色
模型中的数据项可以作为各种角色在其他组件中使用,允许为不同的情况提供不同类型的数据。例如,Qt::DisplayRole用于访问一个字符串,所以可以作为文本显示在视图中。通常情况下,数据项包含了一些不同角色的数据,这些标准的角色由枚举类型Qt::ItemDataRole来定义,常用的角色如下表所列。要查看全部的角色类型,可以在帮助中通过Qt::ItemDataRole关键字查看。
常量 | 描述 |
---|---|
Qt::DisplayRole | 数据被渲染为文本(数据为QString类型) |
Qt::DecorationRole | 数据被渲染为图标等装饰(数据为QColor、QIcon或者QPixmap类型) |
Qt::EditRole | 数据可以在编辑器中进行编辑(数据为QString) |
Qt::ToolTipRole | 数据显示在数据项的工具提示中(数据为QString类型) |
Qt::StatusTipRole | 数据显示在状态栏中(数据为QString类型) |
Qt::WhatThisRole | 数据显示在数据项的“What’s This?”模式下(数据为QString类型) |
Qt::SizeHintRole | 数据项的大小提示,将会应用到视图(数据为QSize类型) |
创建新的模型
当要为一个已经存在的数据结构创建一个新的模型时,需要考虑使用哪种类型的模型来为数据提供接口。如果数据结构可以表示为项目列表或者表格,那么可以子类化QAbstractListModel或者QAbstractTableModel,因为他们为很多功能提供了非常合适的默认实现。如果底层数据结构只能表示为具有层次的树结构,那么就需要子类化QAbstractItemModel。
模型类就是将各种操作转换为对具体数据源的操作,从而对外提供了一个统一的接口供用户使用。如果还想设计一个更复杂的模型,可以参考Qt自带的Simple Tree Model Example示例程序。
视图类
基本概念
在模型/视图架构中,视图包含了模型中的数据项,并将它们呈现给用户,而数据的表示方法可能与底层用于存储数据项的数据结构完全不同。这种内容与表现的分离之所以能够实现,是因为使用了QAbstractItemModel提供的一个标准模型接口,还有QAbstractItemView提供的一个标准视图接口,以及使用了模型索引提供了一种通用的方法来表示数据。视图通常管理从模型获取的数据的整体布局,它们可以自己渲染独立的数据项,也可以使用委托来处理渲染和编辑。
出了呈现数据,视图还处理项目间的导航,以及项目选择的某些方面,如下表分别罗列了视图中的选择行为和选择模式。视图也实现了一些基本的用户接口特性,比如上下文菜单和拖放等。视图可以为项目提供默认的编辑实现,当然也可以和委托一起来提供一个自定义的编辑器。不指定模型也可以构造一个视图,但是在视图显示有用信息以前,必须为其提供一个模型。
常量 | 描述 |
---|---|
QAbstractItemView::SelectItems | 选择单个项目 |
QAbstractItemView::SelectRows | 只选择行 |
QAbstractItemView::SelectColumns | 只选择列 |
常量 | 描述 |
---|---|
QAbstractItemView::SingleSelection | 当用户选择一个项目时,所有已经选择的项目将成为未选择的状态,而且用户无法在已经选择的项目上单击来取消选择 |
QAbstractItemView::ContiguousSelection | 如果用户在单击一个项目的同时按着Shift键,则所有当前项目和单击项目之间的项目都会被选择或者取消选择,这依赖于被单击项目的状态 |
QAbstractItemView::ExtendedSelection | 具有ContiguousSelection的特性,而且还可以按着Ctrl键进行不连续的选择 |
QAbstractItemView::MultiSelection | 用户选择一个项目时不影响其他已经选择的项目 |
QAbstractItemView::NoSelection | 项目无法被选择 |
对于一些视图,如QTableView和QTreeView,在显示项目的同时还可以显示标头。这是通过QHeaderView类实现的,它们使用QAbstractItemModel::headerData()函数从模型中获取数据,然后一般使用一个标签来显示标头信息。通过子类化QHeaderView来设置标签的显示。
Qt中已经提供了QListView、QTableView和QTreeView这3各现成的视图,不过都是使用的规范的格式显示数据。如果想要实现条状图或者饼状图等特殊显示方式,就要重新实现视图类了。
处理项目选择
模型/视图架构中对项目的选择提供了非常方便的处理方法。在视图中被选择的项目的信息存储在QItemSelectionModel实例中,这样被选择项目的模型索引变保持在一个独立的模型中,与所有的视图都是独立的。在一个模型上设置多个模型时,就可以实现在多个视图之间共享选择。
选择由选择范围指定,只需要记录每一个选择范围开始和结束的模型索引即可,非连续的选择可以使用多个选择范围来描述。选择可以看作是在选择模型中保存的一个模型索引集合,最近的项目被称为当前选择。
1.当 前 项 目 和 被 选 择 的 项 目
视图中,总有一个当前项目和一个被选择的项目,两者是两个独立的状态。在同一时间,一个项目可以既是当前项目,同时也是被选择项目。视图负责确保总是有一个项目作为当前项目来实现键盘导航。当前项目和被选择的项目区别如下表所示:
当前项目 | 被选择的项目 |
---|---|
只能有一个当前项目 | 可以有多个被选择的项目 |
使用键盘导航或者鼠标按键可以改变当前项目 | 项目是否处于被选择状态取决于预先定好的模式,例如,单项选择、多重选择等 |
按下F2键或者双击鼠标都可以编辑当前项目 | 当前项目可以通过指定一个范围来一起被使用 |
当前项目会显示焦点矩形 | 被选择的项目会使用选择矩形来表示 |
当操作选择时,可以将QItemSelectionModel看作一个项目模型中所有项目的选择状态的一个记录。一旦设置了一个选择模型,所有的项目集合都可以被选择、取消选择或者切换选择状态,而不需要知道哪一个项目已经被选择了。所有被选择项目的索引都可以被随时进行检错,其他的组件也可以通过信号和槽机制来获取选择模型的改变信息。
2.使 用 选 择 模 型
标准视图类中提供了默认的选择模型,可以在大多数的应用中直接使用。属于一个视图的选择模型可以使用这个视图的selectionModel()函数获得,而且还可以在多个视图之间使用setSelectionModel()函数来共享该选择模型,所以一般不需要重新构建一个选择模型。
委托类
基本概念
与Model-View-Controller模式不同,模型/视图结构中没有包含一个完整分离的组件来处理与用户的交互。一般的,视图用啦i将模型中的数据展示给用户,也用来处理用户的输入。为了获得更高的灵活性,交互可以由委托来执行。这些组件提供了输入功能,而且也负责渲染一些视图中的个别项目。控制委托的标准接口在QAbstractItemDelegate类中定义。
委托通过实现painter()和sizeHint()函数来使它们可以渲染自身的内容。然而,简单的基于部件的委托可以通过子类化QItemDelegate来实现,而不需要使用QAbstractItemDelegate,这样可以使用这些函数的默认项实现。委托的编辑器可以通过两种方式来实现,一种是使用部件来管理编辑过程,另一种是直接处理事件。可以参考Qt提供的Spin Box Delegate Example示例程序来理解第一种形式。如果想继承QAbstractItemDelegate来实现自定义的渲染操作,那么可以参考Pixelator Example示例程序。另外,还可以使用QStyledItemDelegate作为基类,这样可以自定义数据的显示,这个可以参考Star Delegate Example示例程序。
Qt中的标准视图都使用QItemDelegate的实例来提供编辑功能,这种委托接口的默认实现为QListView、QTableView和QTreeView等标准视图的每一个项目提供了普通风格的渲染。标准视图中的默认委托会处理所有的标准角色,具体的内容可以在QItemDelegate类的帮助文档中查看。可以使用itemDelegate()函数获取一个视图中使用的委托,使用setItemDelegate()函数可以为一个视图安装一个自定义委托。
项目视图中的便捷类
从Qt4开始引进了一些标准部件来提供经典的基于项的容器部件,它们底层是通过模型/视图框架实现的。这些部件分别是:QListWidget提供了一个项目列表,QTreeWidget显示了一个多层次的树结构,QTableWidget提供了一个以项目作为单元的表格。它们每一个类都继承了QAbstractItemView类的行为。这些类之所以被称为便捷类,是因为它们使用起来比较简单,适合于少量数据的存储和显示。因为他们没有将视图和模型进行分离,所以没有视图类灵活,不能和任意的模型一起使用,一般建议使用模型/视图的方式来处理数据。
对于这3个便捷类,它们都使用相同的接口提供了一些基于项的特色功能。例如,有时候在项目视图中需要隐藏一些项目而不是删除它们,这可以使用QListWidgetItem类和QTreeWidgetItem提供的setHidden()函数;判断一个项目是否隐藏,可以使用相应的isHidden()函数;可以使用3个便捷类的selectedItems()函数来获取选择的项目,它会返回一个相关项目的列表;还可以使用findItems()函数来进行项目的查找,它也会返回一个相关项目的列表。
其他内容
拖放功能
模型/视图框架完全支持的拖放应用,在列表、表格和树中的项目可以在视图中被拖拽,数据可以作为MIME编码的数据被导入和导出。标准视图可以自动支持内部的拖放,这样可以用来该改变项目的排列顺序。默认的,视图的拖放功能并没有被启用,如果要进行项目的拖动,就需要进行一些属性的设置。如果要在一个新的模型中启动拖放功能,那么还要重新实现一些函数。
代理模型
代理模型可以将一个模型中的数据进行排序或者过滤,然后提供给视图进行显示。Qt中提供了QSortFilterProxyModel作为标准的代理模型来完成模型中数据的排序和过滤,如果要使用一个代理模型,只需要为其设置源模型,然后在视图中使用该代理模型即可。
数据-窗口映射器
数据-窗口映射器QDataWidgetMapper类在数据模型的一个区域和一个窗口部件间提供过了一个映射,这样就可以实现在一个窗口部件上显示和编辑一个模型中的一行数据。