5.1 Model/View结构
5.1.1 Model/View基本原理
GUI应用程序的一个很重要的功能是由用户在界面上编辑和修改数据,典型的如数据库应用程序。在数据库应用程序中,用户在界面上执行各种操作,实际上是修改了界面组件所关联的数据库内的数据。
将界面组件与所编辑的数据分离开来,又通过数据源的方式连接起来,是处理界面与数据的一种较好的方式。Qt使用Model/View结构来处理这种关系,Model/View的基本结构如图5-1。
图5-1 Model/View基本结构(来自Qt帮助文件)
其中各部分的功能是:● Data(数据)是实际的数据,如数据库的一个数据表或SQL查询结果,内存中的一个StringList,或磁盘文件结构等。
● View(视图,或视图组件)是屏幕上的界面组件,视图从数据模型获得每个数据项的模型索引(model index),通过模型索引获取数据,然后为界面组件提供显示数据。Qt提供一些现成的数据视图组件,如QListView,QTreeView和QTableView等。
● Model(模型,或数据模型)与实际的数据通讯,并为视图组件提供数据接口。它从原始数据提取需要的数据,用于视图组件进行显示和编辑。Qt中有一些预定义的数据模型,如QStringListModel可作为StringList的数据模型,QSqlTableModel可以作为数据库中一个数据表的数据模型。
由于将数据源与显示界面通过Model/View结构分离开来,可以将一个数据模型在不同的视图中显示,也可以在不修改数据模型的情况下,设计特殊的视图组件。
在Model/View结构中,还提供了代理(Delegate)功能,代理功能可以让用户定制数据的界面显示和编辑方式。在标准的视图组件中,代理功能显示一个数据,当数据被编辑时,代理通过模型索引与数据模型通讯,并为编辑数据提供一个编辑器,一般是一个QLineEdit组件。
模型、视图和代理之间使用信号和槽通讯。当源数据发生变化时,数据模型发射信号通知视图组件;当用户在界面上操作数据时,视图组件发射信号表示这些操作信息;在编辑数据时,代理发射信号告知数据模型和视图组件编辑器的状态。
5.1.2 Model(数据模型)
所有的基于项数据(item data)的数据模型都是基于QAbstractItemModel类的,这个类定义了视图组件和代理存取数据的接口。数据无需存储在数据模型里,数据可以是其他类、文件、数据库或任何数据源。Qt中与数据模型相关的几个主要的类的层次结构如图5-2所示。
图5-2 Qt中模型类的层次结构
在图5-2中,其中的抽象类是不能直接使用的,需要由子类继承实现一些纯虚函数。Qt提供了一些模型类用于项数据处理,常见的几个见表5-1。
表5-1 Qt提供的数据模型类
Model类 | 用途 |
QStringListModel | 用于处理字符串列表数据的数据模型类 |
QStandardItemModel | 标准的基于项数据的数据模型类,每个项数据可以是任何数据类型 |
QFileSystemModel | 电脑上文件系统的数据模型类 |
QSortFilterProxyModel | 与其他数据模型结合,提供排序和过滤功能的数据模型类 |
QSqlQueryModel | 用于数据库SQL查询结果的数据模型类 |
QSqlTableModel | 用于数据库的一个数据表的数据模型类 |
QSqlRelationalTableModel | 用于关系型数据表的数据模型类 |
数据库相关的三个模型类将在第11章介绍数据库编程时专门介绍。如果这些现有的模型类无法满足需求,用户可以从QAbstractItemModel,QAbstractListModel或QAbstractTableModel继承,生成自己定制的数据模型类。
5.1.3 View(视图组件)
View就是显示数据模型的数据的界面组件,Qt提供的视图组件有:
● QListView:用于显示单列的列表数据,适用于一维数据的操作。
● QTreeView:用于显示树状结构数据,适用于树状结构数据的操作。
● QTableView:用于显示表格状数据,适用于二维表格型数据的操作。
● QColumnView:用多个QListView显示树状层次结构,树状结构的一层用一个QListView显示。
● QHeaderView:提供行表头或列表头的视图组件,例如QTableView的行表头和列表头。
● 视图组件在显示数据时,只需调用视图类的setModel()函数,为视图组件设置一个数据模型就可以实现视图组件与数据模型之间的关联,在视图组件上的修改自动保存到关联的数据模型里,一个数据模型可以同时在多个视图组件里显示数据。
● 在第4章介绍了QListWidget、QTreeWidget和QTableWidget三个可用于数据编辑的组件。这三个类称为便利类(convenience classes),它们分别是三个视图类的子类,其层次关系如图5-3。
图5-3 视图相关类的层次结构图
用于Model/View结构的几个视图类直接从QAbstractItemView继承而来,而便利类则从相应的视图类继承而来。
视图组件类的数据采用单独的数据模型,视图组件不存储数据。便利类则为组件的每个节点或单元格创建一个项(item),用项存储数据、格式设置等。所以,便利类没有数据模型,它实际上是用项的方式集成了数据模型的功能,是将界面与数据绑定了。所以,便利类缺乏对大型数据源进行灵活处理的能力,适用于小型数据的显示和编辑。
5.1.4 Delegate(代理)
代理就是在视图组件上为编辑数据提供编辑器,例如在表格组件中编辑一个单元格的数据时,缺省是使用一个QLineEdit编辑框。代理负责从数据模型获取相应的数据,然后显示在编辑器里,修改数据后,又将数据保存到数据模型中。
QAbstractItemDelegate是所有代理类的基类,作为抽象类,它不能直接使用。其一个子类是QStyledItemDelegate,是Qt的视图组件缺省使用的代理类。
对于一些特殊的数据编辑需求,例如只允许输入整型数,使用一个QSpinBox作为代理组件更恰当,需要从列表中选择数据时使用一个QComboBox作为代理组件更好。这时,就可以从QStyledItemDelegate继承创建自定义代理类。
5.1.5 Model/View结构的一些概念
1. Model/View的基本结构
在Model/View结构中,数据模型为视图组件和代理提供存取数据的标准接口。在Qt中,所有的数据模型类都从QAbstractItemModel继承而来,不管底层的数据结构是如何组织数据的,QAbstractItemModel的子类都以表格的层次结构表示数据,视图组件通过这种规则来存取模型中的数据,但是表现给用户的形式不一样。
图5-4是数据模型的三种常见表现形式。不管数据模型的表现形式是怎么样的,数据模型中存储数据的基本单元是项(item),每个项有一个行号、一个列号、还有一个父项(parent item)。在列表和表格模式下,所有的项都有相同的一个顶层项(root item);在树状结构中,行号、列号、父项稍微复杂一点,但是由这三个参数完全可以定义一个项的位置,从而存取项的数据。
2. 模型索引(modelindex)
为了保证数据的表示与数据存取方式隔离,数据模型中引入了模型索引的概念。通过数据模型能存取的每个数据都有一个模型索引,视图组件和代理都通过模型索引来获取数据。
图5-4数据模型的几种表现形式(来自Qt帮助文件)
QModelIndex是表示模型索引的类。模型索引提供数据存取的一个临时指针,用于通过数据模型提取或修改数据。因为模型内部组织数据的结构可能随时改变,所以模型索引是临时的。如果需要使用持久性的模型索引,则要使用QPersistentModelIndex类。
3. 行号和列号
数据模型的基本形式是用行和列定义的表格数据,但这并不意味着底层的数据是用二维数组存储的,使用行和列只是为了组件之间交互方便而使用的一种规定。通过模型索引的行号和列号就可以存取数据。
要获得一个模型索引,必须提供三个参数:行号、列号、父项的模型索引。例如,对于如图5-4中的表格数据模型中的三个数据项A、B、C,获取其模型索引的代码是:
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
在创建模型索引的函数中需要传递行号、列号和父项的模型索引。对于列表和表格模式的数据模型,顶层节点总是用QModelIndex()表示。
4. 父项(parent item)
当数据模型是列表或表格时,使用行号、列号存储数据比较直观,所有数据项的父项就是顶层项。当数据模型是树状结构时情况比较复杂(树状结构中,项一般习惯于称为节点),一个节点有父节点,也可以是其他节点的父节点,在构造数据项的模型索引时,必须指定正确的行号、列号和父节点。
对于图5-4中的树状数据模型,节点A和节点C的父节点是顶层节点,获取模型索引的代码是:
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
但是,节点B的父节点是节点A,节点B的模型索引由下面的代码生成:
QModelIndex indexB = model->index(1, 0, indexA);
5. 项的角色(item role)
在为数据模型的一个项设置数据时,可以为项设置不同角色的数据。例如,数据模型类QStandardItemModel的项数据类是QStandardItem,其设置数据的函数是:
void QStandardItem::setData(const QVariant &value,int role= Qt::UserRole + 1)
其中,value是需要设置的数据,role是设置数据的角色。一个项可以有不同角色的数据,用于不同的场合。
role是Qt::ItemDataRole枚举类型,有多种取值,如Qt::DisplayRole 角色是在视图组件中显示的字符串,Qt::ToolTipRole是鼠标提示消息,Qt::UserRole可以自定义数据。项的标准角色是Qt::DisplayRole。在获取一个项的数据时也需要指定角色,获取不同角色的数据。
QVariant QStandardItem::data(int role = Qt::UserRole + 1) const
为一个项的不同角色定义数据,可以告知视图组件和代理组件如何显示数据。例如,在图5-5中,项的DisplayRole数据是显示的字符串,DecorationRole是用于装饰显示的属性,ToolTipRole定义了鼠标提示信息。不同的视图组件对各种角色数据的解释和显示可能不一样,也可能忽略某些角色的数据。
图5-5 不同角色数据的表现形式(来自Qt帮助文件)
前一章已经介绍了便利类QListWidget、QTreeWidget和QTableWidget的使用,本章将介绍Model/View结构的基本用法,包括Qt预定义的QStringListModel、QFileSystemModel、QStandardItemModel以及视图组件QListView、QTableView、QTreeView的使用,还介绍如何设计和使用自定义代理。涉及数据库的Model/View的使用在数据库一章单独介绍。