1.架构的选择
1.1是否需要委托
模型视图架构图如下所示,模型视图架构源于MVC模式:模型(Model)是应用对象,表示数据;视图(View)是模型的用户界面,用以显示数据;控制(Controller)定义用户界面对用户输入的反应方式。模型视图架构相对于MVC模式有2个变化:
- 1.将视图与控制两种组件结合在一起,在将数据的存储于数据可视化呈现进行分离同时,提供了较为简单的框架;
- 2.引入了委托(Delegate,代理),用以对用户输入进行灵活处理,可以定制数据的渲染和编辑方式。
由于委托的引入,导致在使用模型视图架构时,存在对委托使用的选择策略问题。
模型中的数据项的可视化一定程度上由其项角色决定,而其项角色比较有限,如下所示。
对于需要除了简单文本、图标等其它可视化呈现方式且需与用户进行较为复杂交互的数据项,采用委托方式,例如对现成的部件自定义委托(QLineEdit、QSpinBox、QComboBox、QProgressBar、QPixmap、QDateTimeEdit、QPushButton等)。委托基类依次包括QAbstractItemDelegate、QItemDelegate、QStyledItemDelegate。
继承QAbstractItemDelegate需实现paint()等方法实现自定义渲染(亦可创建委托控件),注意paint()方法的实现:数据可以另有数据类,作为该委托类的成员,数据类实现paint()方法,但在委托类中调用其数据成员执行paint()。另外其诸多虚方法中存在较多的QStyleOptionViewItem与QModelIndex的参数,便于自定义渲染样式。
相比于QItemDelegate,QStyledItemDelegate是默认的委托实现,更受Qt官方推荐(其使用当前的样式来绘制项目,当要实现自定义的委托或要和Qt样式表一起应用时,强烈建议使用)。2者需实现的几个虚方法主要是:createEditor、setModelData、setEditorData、updateEditorGeometry。需注意在setModelData中完成后发射closeEditor信号,通知视图确保编辑器部件被关闭与销毁,在编辑器编辑完成后发射commitData信号,提交到模型中(亦需发射closeEditor信号)。
Qt官方提供的例子里面强烈建议浏览学习:
- 1.以QStyledItemDelegate为基类的Spin Box Delegate Example、Star Delegate Example;
- 2.以QAbstractItemDelegate为基类的Pixelator Example。
1.2模型/视图架构与图形视图框架的关系
图形视图框架提供了一个基于图形项的图形视图编程方法。由场景(QGraphicsScene)、视图(QGraphicsView)、图形项(QGraphicsItem)3部分组成,其中视图(QGraphicsView)对应视图(View),场景(QGraphicsScene)基本对应模型(Model),图形项(QGraphicsItem)则与委托类有类似之处,其存在交互,也负责自身的渲染(paint等方法)。
2.具体实现
2.1模型的选择策略
2.1.1QAbstractListModel
需要使用QListView显示数据,并配合自定义model时,我们从此类继承。仅用于一维数据模型。
子类化时,我们只需要实现rowCount()函数即可返回列表中的项目数,实现data()函数从列表中检索项目。
由于该模型代表一个一维结构,因此rowCount()函数返回模型中的项目总数。表现良好的模型还提供了HeaderData()实现。对于可编辑列表模型,还必须提供setData()的实现,并实现flags()函数,以便它返回包含Qt::ItemIsEditable的值。为可扩展列表的数据结构提供接口的模型可以提供insertRows()和removeRows()的实现。在实施这些功能时,重要的是调用适当的函数,以便所有连接的视图都知道任何更改:
insertRows()实现必须在将新行插入数据结构之前调用beginInsertRows(),并且之后必须立即调用endInsertRows()。
removeRows()实现必须在将行从数据结构中删除之前调用beginRemoveRows(),并且必须立即调用endRemoveRows()。
Qt官方提供了其的一个派生类QStringListModel,存储一个简单的QString项目列表。
2.1.2QAbstractTableModel
需要使用QTableView显示数据时,并配合自定义model时,我们从此类继承。仅用于二维数组模型。
rowCount()和columnCount()函数返回表的尺寸。要检索与模型中项目相对应的模型索引,请使用index()并仅提供行和列号。子类化QAbstractTableModel时,必须实现rowCount(),columnCount()和data()。 QAbstractTableModel提供了index()和parent()函数的默认实现。表现良好的模型还将实现HeaderData()。可编辑的模型需要实现setData(),然后实现flags()返回包含Qt::ItemIsEditable的值。为可扩展数据结构提供接口的模型可以提供insertRows(),removeRows(),insertColumns()和removeColumns()的实现。在实施这些功能时,重要的是调用适当的函数,以便所有连接的视图都知道任何更改:
insertRows()实现必须在将新行插入数据结构之前调用beginInsertRows(),并且之后必须立即调用endInsertRows()。
insertColumns()实现必须在将新列插入数据结构之前调用beginInsertColumns(),并且此后必须立即调用endInsertColumns()。
removeRows()实现必须在将行从数据结构中删除之前调用beginRemoveRows(),并且此后必须立即调用endRemoveRows()。
removeColumns()实现必须在将列从数据结构中删除之前调用beginRemoveColumns(),并且此后必须立即调用endRemoveColumns()。
QAbstractTableModel的一系列派生类QSqlQueryModel、QSqlTableModel、QSqlRelationTableModel,用来访问数据库。此处不赘述。
2.1.3QFileSystemModel
提供本地文件系统中文件和目录的信息。
2.1.4QStandardItemModel
可以作为QListView、QTableView、QTreeView的标准model。管理复杂的树型结构数据项,每个数据项可以包含任意的数据。
本模型具有内置的Item类:QStandardItem类,提供了一个用于QStandardItemModel类的项。Item通常包含文本,图标或复选框。每个Item都可以具有自己的背景刷,并使用setbackground()函数设置。当前的背景刷可以使用background()找到。每个Item的文本标签可以用自己的字体和刷子呈现。这些用setFont()和setForeground()函数指定,并使用font()和foreground()读取。默认情况下,项目是enabled, editable, selectable, checkable,并且可以用作drag、drop操作的来源,也可以用作drop的目标。可以通过调用setFlags()来更改每个Item的标志。可以使用setCheckState()函数检查可检查的Item。相应的checkState()函数指示是否已选中该Item。
可以通过调用setData()将特定于应用程序的数据存储在Item中。每个项目都可以包含二维子项表。这使得建立Item的层次结构成为可能。典型的层次结构是树,在这种情况下,子表是一个带有单列(列表)的表。
可以使用setRowCount()和setColumnCount()设置子表的尺寸。项目可以用setChild()将Item放在子表中。通过childe()方法获取指向子项的指针()。新的行和列也可以用insertRow()和insertColumn()插入,也可以与appendRow()和appendColumn()插入。使用附加功能和插入功能时,子表的尺寸将根据需要增长。
现有的子项可以使用removeRow()或takeRow()删除;相应地,可以使用removeColumn()或takeColumn()删除列。
可以通过调用sortChildren()对项目的孩子进行分类。
当子类化用于提供自定义项目时:
可以为它们定义新类型,以便将它们与基类别区分开。应重新实现type()函数以返回等于或大于UserType的新类型值。
如果要执行数据查询的自定义处理和/或控制项目\的数据表示如何表示,则重新实现data()和setData()。
如果您希望QstandardItemModel能够按需创建自定义项目类的实例,需重新实现clone()。
如果要控制项目的序列化形式表示如何表示项目,请重新实现read()和write()。
如果要控制项目比较的语义,则重新实现operator<()。operator<()决定了用sortChildren()或QStandardItemModel:: sort()对Item进行排序顺序。
具有以下优点:
- 1.实现代码简单,QStandardItemModel可以使用QStandardItem,通过不断添加子节点,从而构建出list、table、tree结构的数据。;
- 2.该类使用QStandardItem存放数据项,用户不必定义任何数据结构来存放数据项;
- 3.QStandardItem使用自关联关系,能够表达列表、表格、树甚至更复杂的数据结构,能够涵盖各种各样的数据集;
- 4.QStandardItem本身存放着多个『角色,数据子项』,视图类、委托类或者其他用户定义的类能够方便地依据角色访问各个数据子项。
缺点: - 1.当数据集中的数据项很多时,施加在数据集上的某些操作的执行效率会很低;
- 2.数据太大时,占用内存巨大,性能低下。
QStandardItemModel vs QAbstractItemModel
- 1.对于数据量小且不需要更新的场景,我们使用QStandardItemModel来实现比较简单,没有自定义model那么多代码逻辑。
- 2.在数据量小,但是需要更新情况下,我们采用自定义model来实现,即使数据量小,更新数据其实也是比较慢的,它会占用较多UI线程时间,如果其他线程业务繁重,就会影响UI线程性能,导致界面卡顿。
- 3.在数据量大情况下,无论更新与否,我们都采用自定义model来实现。
2.1.5QAbstractItemModel(建议重点关注)
需要使用QTreeView显示数据时,并配合自定义model时,我们从此类继承。
本模型为所有模型的祖宗基类。其定义了一个标准接口,供视图和委托来访问数据,数据本身不一定存储于模型中,也可以存储于数据结构、类、文件、数据库、其它组件内。其为数据提供了十分灵活的接口来处理各种视图,这些视图可以将数据表现为表格(table)、列表(list)、树(tree)等形式。对于一般的表格或者列表的数据结构,可以采用子类化QAbstractTableModel、QAbstractListModel或采用Qt官方提供的这2者的现场的派生模型来处理特定类型。对于更为通用的模型,以上的现成模型无法满足需求,则需自定义数据节点类(不强制继承QObject,但角色类似QStandardItem),形成树形的数据结构,与模型派生类配合完成模型层的功能。
个人建议复杂一些的数据,直接采用QAbstractItemModel派生类+自定义数据节点类。
基础数据模型将视图和委托作为表的层次结构。如果不利用层次结构,则模型是一个简单的行和列表。每个项目都有QModelIndex指定的唯一索引。
可以通过模型访问的每个数据都有关联的模型索引。您可以使用index()函数获得此模型索引。每个索引可能具有sibling()索引;子项目具有parent()索引。
每个项目都有与之关联的许多数据元素,可以通过将role(请参见Qt::ItemDataRole)指定到模型的data()函数来检索。所有可用角色的数据可以使用itemData()函数同时获得。
可使用特定的Qt::ItemDataRole设置每个角色的数据。使用setData()单独设置单个角色的数据,也可以使用setItemData()设置所有角色。
可以使用flags()(请参阅Qt::ItemFlag)查询项目,以查看是否可以以其他方式选择,拖动或操纵它们。
如果项目具有子对象,则haschildren()返回相应的索引。
该模型在每个层次结构的每个级别上都有一个rowCount()和columnCount()。可以使用InserstRows(),insertColumns(),removeRows()和removeColumns()插入和删除行和列。
该模型发出信号以指示变化。例如,每当更改模型提供的数据项目时,都会发出dataChanged()。模型提供的标头更改会导致headerDataChanged()发出。如果基础数据的结构更改,则该模型可以发出LayoutChanged(),以指示任何附带的视图,应重新显示所显示的任何项目,考虑到新的结构。
可以使用Match()函数搜索通过模型可用的项。
要对模型进行排序,可以使用sort()。
在将QAbstractItemModel进行子类化时,至少必须实现index(),parent(),rowcount(),colundCount()和data()。这些功能用于所有只读模型,并构成可编辑模型的基础。
由于rowCount()的实现很昂贵,还可以重新实现haschildren()为实现模型特殊行为。这使得模型可以限制视图要求的数据量,并可以用作实施懒惰模型数据的一种方式。
要在模型中启用编辑,您还必须实现setData()和重新实现flags()以确保返回ItemIsEditable。还可以重新实现headerData()和setHeaderData()来控制模型的标头的显示方式。
分别重新实现setData()和setHeaderData()函数时,必须显式发射dataChanged()和headerDatachanged()信号。
自定义模型需要为其他组件创建模型索引。为此,请用适合该项目的行和标识符来调用CreateIndex(),并以指针或整数值作为其标识符。这些值的组合对于每个项都必须是唯一的。自定义模型通常在其他重新实现功能中使用这些唯一标识符来检索项目数据并访问有关该项的父和子的信息。有关唯一标识符的更多信息,请参见Simple Tree Model Example示例。
不必支持Qt::ItemDataRole中定义的每个角色。取决于模型中包含的数据类型,仅实现data()函数以返回某些更常见的角色的有效信息可能才是有用的。大多数模型至少提供了Qt::DisplayRole的项目数据的文本表示,并且举止良好的模型还应为Qt::ToolTipRole、Qt::WhatsThisRole提供有效的信息。支持这些角色使模型可以与标准QT视图一起使用。但是,对于某些处理高度专业数据的模型,可能仅为用户定义的角色提供数据才是合适的。
为可扩展数据结构提供接口的模型可以提供insertRows(),removeRows(),insertColumns()和removeColumns()的实现。在实现这些功能时,重要的是要在发生之前和之后需向一切的连接视图通知有关模型尺寸更改:
inserstRows()实现必须在将新行插入数据结构之前调用beginInsertRows(),完成后立即调用endInsertRows()。
insertColumns()实现必须在将新列插入数据结构之前调用beginInsertColumns(),完成立即调用endInsertColunms()。
removeRows()实现必须在从数据结构中删除行之前调用beginRemoveRows(),完成立即调用endRemoveRows()。
removeColumns()实现必须在将列从数据结构中删除之前调用beginRemoveColumns(),完成立即调用endRemoveColumns()。
这些功能发出的私人信号使附件的组件有机会在任何数据都无法使用之前采取行动。使用这些开始和结束功能的插入和删除操作的封装也使模型可以正确管理持久模型索引(persistent model indexes)。如果希望选择正确处理,则必须确保调用这些功能。如果您插入或删除带子项的项,则无需为这些子项调用这些方法。换句话说,parent项将处理其子项。
要创建逐渐填充的模型,您可以重新实现fetchMore()和canFetchMore()。如果fetchMore()的重新实现将行添加到模型中,则必须调用beginInsertRows()和endInsertRows()。
Qt官方提供的例子里面强烈建议浏览学习(均为自定义数据结构):
- 1.Simple Tree Model Example;
- 2.Editable Tree Model Example。
2.2模型与数据的关系
模型项分为Qt定义的标准Item和用户自定义数据节点类,其特点都是完整的数据结构是树及其变种,另外自定义数据节点类需提供辅助功能接口,确保其对应的模型类能实现所必须的几个派生虚方法。
2.3内部通信方式
模型、视图、委托见使用信号槽实现通信:
- 1.当数据源的数据发生改变时,模型发出信号告知视图;
- 2.当用户与显示的项目交互时,视图发出信号来提供交互信息;
- 3.当编辑项目时,委托发出信号,告知模型和视图编辑器的状态。
各模型、委托、视图存在大量的信号槽,需详细参考Qt帮助,比如QAbstractItemModel中更新数据后,记得发送信号dataChanged,显式通知视图刷新显式。
2.4代理模型
QAbstractProxyModel类为代理项目模型提供了一个可以执行排序,过滤或其他数据处理任务的基类。
此类定义了代理模型必须使用的标准接口,以便能够与其他模型/视图组件正确互动。它不应该直接实例化。
所有标准代理模型均来自QAbstractProxyModel类。如果您需要创建一个新的代理模型类,通常最好子类化一个已经存在且提供最接近期望的行为的类。
应该通过使用或子类化QSortFilterProxyModel创建过滤或从源模型中分类数据的代理模型。
要子类化QAbstractProxyModel,您需要实现mapFromSource()和mapToSource()。只有在需要不同于默认行为的行为时,才需要重新实现mapSelectionFromSource()和mapSelectionToSource()函数。
注意:如果源模型被删除或没有指定源模型,代理模型将在一个空占位符模型上操作。
2.5数据-窗口映射器
QDataWidgetMapper类在数据模型的一个区域和一个窗口部件间提供了一个映射,即可在一个窗口部件上显示和编辑一个模型中的一行数据。
2.6模型/视图架构下的style
以QTreeView举例,其绘制3剑客(QStyleOptionViewItem 样式直接决定渲染效果,需详加考察)
drawBranches(QPainter *, const QRect &, const QModelIndex &) const
drawRow(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const
drawTree(QPainter *, const QRegion &) const
QTableView就没有这些可override的绘制虚方法,间接证明其通用性、可扩展性远远不如QTreeView。切记切记!
3.模型子类化
强烈建议阅读Qt官方的Model/View Programming,其中的模型子类化(Model Subclassing Reference)一节尤其需要细读。部分翻译如下:
模型子类需要提供在QAbstractItemModel基类中定义的许多虚函数的实现。需要实现的这些函数的数量取决于模型的类型——它是为视图提供一个简单的列表、一个表还是一个复杂的项目层次结构。从QAbstractListModel和QAbstractTableModel继承的模型可以利用这些类提供的函数的默认实现。在树状结构中公开数据项的模型必须为QAbstractItemModel中的许多虚拟函数提供实现。
需要在模型子类中实现的函数可以分为三组:
- 1.项目数据处理:所有模型都需要实现一些功能,使视图和委托能够查询模型的维度、检查项目和检索数据。
- 2.导航和索引创建:分层模型需要提供函数,视图可以调用这些函数来导航它们公开的树状结构,并获取项目的模型索引。
- 3.拖放支持和MIME类型处理:模型继承了控制内部和外部拖放操作执行方式的函数。这些函数允许用其他组件和应用程序可以理解的MIME类型描述数据项。
TODO
4.参考资料
【1】《Qt Creator快速入门 第3版》.霍亚飞
【2】 QTreeView使用系列教程.百里杨
【3】Qt助手