Qt的模型/视图编程方法(model/view programming)
由于最近做的一个程序需要用一个视图显示所定义的数据,翻了下Qt的widget,有QTreeWidget,QTableWidget,QListWidget。但是这些widget内部包含了存储这些数据的container,也就是说用户产生的数据为了能够以列表或者树目录的形式显示出来,必须将其拷贝一份。这就造成了数据的冗余,对于数据不是很多的情况牺牲一点点空间也无妨,但是对于数据库等大型数据来讲,这便是无法原谅的了。
翻了翻Qt 的帮助文档,Qt提供了一个框架完美得解决了这个问题:Model/View Programming。这种方法采用了将视图和数据分离的策略,视图只负责显示,不提供数据存储,而model中存储了自定义的数据,这样仅一份数据可以在多个视图中显示。这个思想类似于C++中容器和算法,一个存储数据,一个使用数据,它们之间的接口就是遍历器。
本文算是学习笔记,网上的帮助文档加上自己实践花了1个多星期,在此总结一下。
Qt 4引入了一系列基于模型/视图构架的类,用于管理数据及其呈现之间的关系。由这个构架带来的功能的分离能够使得开发者更加灵活地自定义数据的呈现方式。Qt也提供了一些标准的接口,能够使大部分数据可以用已有的视图来显示。在帮助文档中主要介绍了这些模块的大概,基本概念和基本函数的用法。给出了每一个部件的用法,也提供了一些例子。
模型/视图构架
模型-视图-控制器(MVC)是一个基本的设计模式。这种设计模式定义了3个部件。模型即应用实体,视图就是屏幕呈现,控制器定义了用户接口和对用户输入的反馈。在MVC之前,用户界面的设计将这些模块糅合到了一起。MVC从功能上将3个部件分开,从而提供了更多的灵活性和复用性。
如果讲视图和控制器结合到一起,结果就是模型/视图构架。这种构架仍然将数据的存储和用户的呈现分开,能够使同一数据用不同的视图来显示,而不用改变内在的数据结构。为了能够灵活把握用户输入,Qt引入了代理(delegate)的概念。代理能够在视图中灵活定义用户将用哪种方式输入数值,是用LineEdit直接输入,还是用ComboBox选择,还是用SpinBox调节。
模型与数据源想关联,并且提供了接口供构架中的其他模块访问数据,至于如何访问数据,决定于数据的类型和模块是如何实现与数据的相关的。视图从模型中获得数据的索引(index,专有类QModelIndex),这些索引是对数据的引用。在视图中,当数据元素需要编辑时,代理便负责提供编辑的接口,代理直接与模型通信,并使用模型提供的索引。
模型,视图和代理通过信号(signals)和槽(slots)相互通信:来自模块中的信号通知视图数据源中数据的改变。来自视图的信号提供了用户交互和数据显示方式的信息。当用户编辑数据时,代理发出信号,提示模型和视图编辑的状态等信息。
先来看一下最基本的三个类,Model/View框架中其他的类都是派生自这三个基本类。
QAbstractItemModel 这个类就是Model的抽象接口
QAbstractItemView 这个是视图的抽象接口
QAbstractItemDelegate View和model的“中间接口”,当你需要在视图中编辑item时,这个类就派上用场了。
模型
模型提供了接口给视图和代理,但有一点要清楚,它并不提供数据的存储。数据并不存储在模型中,而存储在由一些数据结构中或者类所定义的另外的容器中,例如文件,数据库,或者别的应用程序部件中。
QAbstractItemModel提供了足够的接口供表格,树形目录,列表等显示数据。但要用特定视图显示数据时,最好从QAbstractListModel、QAbstractTableModel中派生,它们提供了对一些常用函数更合适的默认实现。这些类通过子类化之后,能提供更加自定义的,更加特定的列表,表格视图。(一般子类化这三个抽象类)
Qt提供了一些已经完成的模型类来处理数据:
l QStringListModel:用列表来处理QString的数据。
l QStandardItemModel管理了复杂的树形结构。
l QFileSystemModel提供了本地文件系统中的文件和目录信息。
l QSqlQueryModel,QSqlTableModel,QSqlRelationalTableModel是用来存取数据库的。
视图
已经定义好的视图有3个:QListView,QTableView,QTreeView。从名称可以看出来它们各自的功能,以后会一一介绍。这三个类都基于抽象类QAbstractItemView。
代理
代理的抽象类是QAbstractItemDelegate,基于它的标准实现是QStyledItemDelegate,这个代理被用在以上3个标准视图中。然而还有一个代理QItemDelegate,它与QStyledItemDelegate是完全独立的两个代理类。因为QStyleItemDelegate使用当前风格绘制元素,所以当你用自定义代理或者使用Qt样式表的时候,最好是子类化这个。
使用模型和视图
下面将介绍如何使用模型/视图模式。
两个标准模型
Qt已经为我们实现了2种模型,QStandardItemModel和QFileSystemModel。QStandardItemModel是一个多用途的模型,可以用来表示多种不同类型的数据,可以显示在列表,表格,树视图中。这个模型存储了数据元素。
QFileSystemModel是一个维持了内容路径的模型,它自己不存储任何数据元素,仅仅表示了本地文件系统中的文件和路径。它拿来就能用,并能非常容易地设置好药显示的数据,用这个模型可以示范一下如何搭建一个可用使用的视图,同时也可以看看如何操作模型中数据的索引。
使用已经存在的模型搭建视图
QListView和QTreeView比较适合用来显示路径信息。下图中左边用了树状图,右边是列表视图。并且两个视图共享了用户的选择(即选择一个视图中的一个项目,另一个视图中的项目会同时被选中),选择项目牵扯到另外一些类,后面有详细探讨。
代码很简单:
这里index()的用法是专门针对QFileSystemModel的,给它一个路径,它返回一个QModelIndex值。这个类后面会讨论,专门用于对模型中每个项目的索引操作。
int main(int argc, char *argv[]) { QApplication app(argc, argv); QSplitter *splitter = new QSplitter;//QSplitter用户分割两个widget 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(); }
模型类
到这里可能你还对模型视图框架中的基本概念还不是很清楚,这数据如何在model中存储呢?视图到底如何从Model中获取数据?让我来看看基本概念。
首先模型为视图和代理提供了一个标准接口用于存取数据,它就是QAbstractItemModel类。无论数据项是用什么数据结构存储的,QAbstractItemModel的子类(作为一个抽象类,你总要子类化一下才能用吧)总是用一种抽象的表结构来表示各个数据项。下面的图表示了数据的存储结构,可以发现数据都是以行、列的形式表示的,list和tree也可以有多个列。由此可以发现,其实ModelIndex对象里面应该有一个行号和一个列号用于表示其中一个数据项目。
当用setModel()设置好一个视图的model之后,模型和视图之间的联系通过信号和槽联系,当数据改变后,不管是在Model中改变的还是在视图中改变的,它们都能发出信号通知对方。
既然Model index与模型中数据表示息息相关,那就先讲这个模型索引。
模型索引(model index)
为什么要引入一个index来连接视图和模型呢?视图直接操作模型中的数据吗?Qt为了使它们分工明确,视图只管显示,模型只管存储,于是引入一个model index来存取数据提供给视图。于是视图类中只要定义一个接口函数,其参数为一个QModelIndex就行了,我不管里面的数据是什么样子的。代理类(delegate)也一样。
这样做的结果就是,只需要模型知道数据如何获得就可以了,另外一点就是模型操作的数据类型可以一般化定义,就是QVarient类(可以表示很多不同的Qt标准化类型)。Model index数据结构中也有一个其指代的模型的指针,这样使得有多个模型要显示时不至于混乱。
QAbstractItemModel *model = index.model();//获取index指代的模型
Model index获得的数据信息只是临时的,因为model可以实时地了解内部数据结构,但是index作为一个买了东西就走的顾客,可不知道你这商店里面到底有什么类型东西。于是Qt提供了一个回头客——QPersistentModelIndex,据说可以更新model中的信息,这个以后再说。一般的index由类QModelIndex表示。
Index要知道数据项目的信息,有3个属性是要先知道的,那就是数据的行数,列数,还有项目的父项目的index。对于list或者table,父节点可能不是那么要紧,但是对于树形结构,就必须要一个父节点才能很好地显示数据了。一般来讲,最顶层的数据元素,它的父节点一遍可以用QModelIndex来表示,即:
QModelIndex indexA = model->index(0, 0, QModelIndex()); QModelIndex indexB = model->index(1, 1, QModelIndex()); QModelIndex indexC = model->index(2, 1, QModelIndex());
以上是下图table model中数据索引的获得方法。三个参数分别是行,列和父项目索引。行号和列号都是从0开始,这很符合2维数组的下标表示方法。但是很多数据又不一定是行列形式的,数据结构多种多样,如何才能将多种多样的数据结构转变成这种易于显示的表格结构呢?在如何使用model中将会介绍。
这是index()函数格式:
QModelIndex index = model->index(row, column, parent);
对于tree的写法也很简单,只要换一个父亲节点的索引就可以了:
QModelIndex indexA = model->index(0, 0, QModelIndex()); QModelIndex indexB = model->index(1, 0, indexA); QModelIndex indexC = model->index(2, 1, QModelIndex());
数据元素显示方式(Item roles)
一个模型里面的数据可以用很多不同的表示方式显示出来,比如Qt::DisplayRole是用于显示字符串数据的,不管你的真实数据是什么,它都自动变成一个字符串显示在view中。标准的显示方式由Qt::ItemDataRole表示。常用的还有一种Qt::DecorationRole,用于显示颜色QColor,图标QIcon和小图片QPixmap。当然你可以自定义Role了,不过这个以后再讲。
我们可以直接通过两个参数获得我们想要的数据:index和Role:
QVariant value = model->data(index, role);
总结这部分:
l 模型索引提供视图和代理数据元素的位置信息,并且使之与数据依赖的数据独立。
l 数据通过行,列及父节点3个参数定位。
l 模型索引由需要模型中数据的部件如视图和代理构建产生。
l 当指定一个父节点且这个父节点有效,并用index()函数索引一个数据元素时,你将获得这个父节点的自节点元素。当父节点无效时,你只会获得最高层数据元素。
l Role区分了同一个数据元素不同类型的数据。
如何使用模型索引,讲在下一篇讲到...