第13章 Model/View框架
具有图形用户界面的应用程序常需要使用一些控件来显示程序中的数据,或者接收用户输入的数据。一种设计策略是令控件既负责存储程序中的数据,又负责显示或者编辑这些数据。这种策略虽然简单、直观,但是难以复用、灵活性差。例如,设有两个控件,一个负责显示一系列的字符串,另外一个负责显示一系列整数。虽然这两个控件在外观、处理用户交互命令等方面有诸多相似之处,但是由于它们所含数据的类型不同,两者无法复用对方的代码。还比如,设一个控件所要处理的数据量很大,用户需要使用多个控件以查看不同部分的数据。由于在这种设计策略下,数据被嵌入在控件内部,我们只能创建多个相同类型的控件,每个被用来显示不同部分的数据。但是这会导致大量数据的重复存放。
模型-视图-控制器架构(Model-View-Control structure,MVC)可以很好地解决这一问题。该架构将数据及其显示分离开来,使用模型存储、访问数据,使用视图显示数据,使用控制器处理用户的交互命令。无论采用什么数据结构存放数据,模型总会提供一个统一的数据访问接口,我们将其称为"模型访问接口"。使用该接口,一个视图可被用来显示不同模型中的数据,如图13 1左半部分所示,这提高了视图的可复用性。当需要查看大量数据的不同部分时,可以采用图13 1右半部分的结构,令多个视图通过模型访问接口,访问不同部分的数据并显示。数据虽然被显示在多个不同的视图中,但是程序并没有重复存放这些数据。当视图A、B、C是不同类的对象时,它们将以不同的方式显示模型中的数据,这往往是用户所期待的。
|
| (点击查看大图)图13 1 MVC架构的优势 |
标准MVC架构中,控制器处理用户的交互命令,视图仅负责数据的显示。Qt中的Model/View框架实现了标准MVC架构的功能,但是它的体系结构和标准MVC有些差别。如图13 2所示,Qt的Model/View框架由模型、视图、委托(delegate)组成。其中,模型负责存储与访问数据,这和MVC中的一致。数据的显示被分为两个部分:每个数据项的绘制由委托完成,其他的绘制工作由视图完成。对用户交互命令的处理也被分为两个部分:对每个数据项的编辑命令由委托完成,其他的交互命令由视图完成。或者说,视图负责总体的绘制与交互,委托仅负责单个数据项的绘制与交互。这样的职责划分和MVC中的并不一致。
|
| (点击查看大图)图13 2 Model/View框架的总体结构 |
具体地说,Model/View框架中的数据集(dataset)由若干个数据项(item)组成,每个数据项除了存放应用程序所要显示的数据之外,还存放着框架中其他部分需要访问的数据,比如这个数据项的显示颜色。无论数据集以什么数据结构存放,模型(model)总是将它看作一棵树,每个树节点就是一个数据项。具有列表或者表格结构的数据集被看作只含最顶层节点、不含任何子节点的树。"模型"提供统一的编程(www.cppentry.com)接口,供视图(view)、委托(delegate)或者应用程序本身获取这棵树的结构信息以及每个节点的数据。
"视图"负责绘制总体外观并处理用户的交互命令,但它并不具体负责每个数据项的绘制与编辑。对于具有列表或者表格结构的数据集,视图计算各数据项的位置,绘制每列上方或者每行左侧的标头(header)。对于具有树状层次结构的数据集,视图还绘制树状的层次结构。视图负责处理用户的浏览、选择等交互命令。例如,用户使用键盘或者鼠标选择一些数据项时,视图能够记录哪些数据项被选择。
"委托"负责绘制每个数据项,并创建"编辑器"控件对数据项进行编辑。默认情况下,Model/View框架会创建一个委托对象,负责绘制与编辑各种类型的数据项。程序员也可以创建新的委托对象,以截然不同的方式对某些数据项进行绘制与编辑,提高了框架的灵活性,这是引入"委托"的主要目的。当用户希望编辑某个数据项时,委托对象并不会亲自处理这个任务,它会依据被编辑数据项的数据类型,创建一个编辑器控件,并对目标数据项进行编辑。编辑完成后,委托对象负责将编辑器中的数据写回模型。
图13 3中的例子具体演示了框架各个部分的分工。数据集含有4行、2列数据项,视图对象计算每个数据项位置,绘制水平、垂直方向的标头。委托对象负责绘制每个数据项。当用户双击一个数据项时,一个编辑器控件负责对其进行编辑。
|
| 图13 3 使用Model/View框架的一个例子 |
负责实现模型、视图、委托功能的类分别被称为模型类、视图类、委托类,这些类的对象分别被称为模型对象、视图对象、委托对象。Qt的Model/View框架比较复杂,涉及36个类。本章13.1节介绍该框架的总体结构,主要类之间的协作关系以及各个类的主要职责。为了使用Model/View框架,程序员应该将数据集封装在一个模型类中,并实现模型访问接口,以供视图类或者委托类访问。13.2节介绍了这个实现过程。这一节是理解Model/View框架的关键。
视图类通过模型访问接口访问应用程序的数据,与数据的存储、访问细节没有任何关系,因而,在最简单的情形下,程序员只需创建一个视图对象,该对象即可显示模型中的数据集。当然,程序员也可以调用视图对象的成员函数,控制视图的外观(比如标头文字等)。13.3节介绍了视图类的功能与使用方法。
用户在浏览一个视图时,常会选择某些数据项,以对它们做进一步的操作(比如删除、拖曳等)。视图对象能够处理与选择操作相关的交互命令,记录哪些数据项被选择,并利用Qt的信号与槽机制通知应用程序数据项的选择状态发生了变化。应用程序所要做的是读取数据项的选择状态并做进一步处理。13.4节将讨论这部分内容。
大多数情况下,程序员只需利用13.2~13.4节讨论的知识即可顺利地使用Model/View框架,无需关心该框架中的"委托",这是由于每个视图对象会指向一个默认的委托对象,后者能完成常见数据项的绘制与编辑任务。然而,当程序员需要彻底地控制数据项的绘制,或者需要使用新的编辑器来编辑某些数据项时,就需要创建新的委托类,13.5讨论这一话题。
某些情况下,我们希望对模型中的数据集做一些临时处理(比如排序、筛选)之后,再送给视图对象显示,但是我们又不希望在物理上修改数据集。Model/View框架中的代理模型(proxy model)可以完成这个任务,13.6节讨论相关的类。
在前面这些小节的讨论中,为了使用Model/View框架,我们至少需要创建一个模型对象、一个视图对象。如果数据集很简单,我们也可以使用13.7节讨论的"便利视图类"对数据集进行处理。便利视图类在其内部定义了模型对象,并提供了简洁、易用的接口来操作模型中的数据集。同时,作为视图类,它们也能显示模型中的数据集。因此,我们只需定义一个便利视图类的对象,即可使用Model/View框架。