Qt Model-View架构领悟

1.架构的选择

1.1控件的区别

Mode/View是一种用于在处理数据集的控件中分离数据和视图的技术。标准控件不是为将数据与视图分离而设计的,这就是为什么Qt有两种不同类型的控件。两种类型的控件看起来都一样,但它们与数据的交互方式不同。
标准控件使用的数据是作为控件一部分的 在这里插入图片描述

视图(View)类操作外部数据(模型) 在这里插入图片描述
比较:

  • 1.标准的表格控件:表格控件是用户可以更改的数据元素的二维数组。通过读写表格控件提供的数据元素,表格控件可以集成到程序流中。这种方法在许多应用程序中非常直观和有用,但是使用标准表格控件显示和编辑数据库表可能会有问题。必须协调两个数据副本:一个在控件外部;一个在控件内部。开发人员负责同步两个版本。除此之外,表示和数据的紧密耦合使得编写单元测试更加困难。
  • 2.Mode/View提供了一个使用更通用的体系结构的解决方案。Mode/View消除了标准控件可能出现的数据一致性问题。Mode/View还可以更容易地使用同一数据的多个视图,因为一个模型可以传递给多个视图。最重要的区别是Mode/View控件不在表单元格后面存储数据。实际上,它们直接从您的数据操作。因为视图类不知道数据的结构,所以需要提供一个包装器,使数据符合QAbstractItemModel接口。视图使用此接口读取和写入数据。实现QAbstractItemModel的类的任何实例都称为模型。一旦视图接收到指向模型的指针,它将读取和显示其内容,并成为其编辑器。

1.2是否需要委托

模型视图架构图如下所示,模型视图架构源于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.3模型/视图架构与图形视图框架的关系

图形视图框架提供了一个基于图形项的图形视图编程方法。由场景(QGraphicsScene)、视图(QGraphicsView)、图形项(QGraphicsItem)3部分组成,其中视图(QGraphicsView)对应视图(View),场景(QGraphicsScene)基本对应模型(Model),图形项(QGraphicsItem)则与委托类有类似之处,其存在交互,也负责自身的渲染(paint等方法)。

由于QCharts与图形视图框架存在继承关系,因此实际上,QCharts也是模型/视图架构。

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()实现必须在将新列插入数据结构之前调用beginInsertCo​​lumns(),并且此后必须立即调用endInsertColumns()。
	removeRows()实现必须在将行从数据结构中删除之前调用beginRemoveRows(),并且此后必须立即调用endRemoveRows()。
	removeColumns()实现必须在将列从数据结构中删除之前调用beginRemoveColumns(),并且此后必须立即调用endRemoveColumns()。

QAbstractTableModel的一系列派生类QSqlQueryModel(封装SQL结果集)、QSqlTableModel(封装SQL表)、QSqlRelationTableModel(封装带外键的SQL表),用来访问数据库。此处不赘述。

2.1.3QFileSystemModel

提供本地文件系统中文件和目录的信息。

与之类似的还有QDirModel。

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.1.6QSortFilterProxyModel

排序和/或筛选其他模型

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类在数据模型的一个区域和一个窗口部件间提供了一个映射,即可在一个窗口部件上显示和编辑一个模型中的一行数据。其实质是表和模型之间的适配器
与QDataWidgetMapper类似, Qt中的QCompleter能够自动完成适配,例如QComboBox、QLineEdit。 QCompleter使用模型作为其数据源。

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类型描述数据项。

3.1项目数据处理

模型可以对其提供的数据提供不同程度的访问。它们可以是简单的只读组件,一些模型可能支持调整大小的操作,其他模型可能允许编辑项目。

3.1.1Read-Only access

为了提供对模型提供的数据的只读访问,可以使用以下功能。必须在模型的子类中实现。

flags():
被其他组件用来获取模型提供的每个项目的信息。在许多模型中,标志的组合应该包括Qt::ItemIsEnabled和Qt::ItemIsSelectable。

data():
用于向视图和委托提供项目数据。通常,模型只需要为Qt::DisplayRole和任何特定于应用程序的用户角色提供数据,但为Qt::ToolTipRole、Qt::AccessibleTextRole和Qt::AccessibleDescriptionRole提供数据也是一种很好的做法。有关与每个角色关联的类型的信息,请参阅Qt::ItemDataRole枚举文档。

headerData():
为视图提供显示在其标题中的信息。该信息仅由能够显示页眉信息的视图检索。

rowCount():
提供模型暴露的数据行数。

这四个函数必须在所有类型的模型中实现,包括列表模型(QAbstractListModel子类)和表模型(QAbstractTableModel子类)
此外,还有以下功能must在QAbstractTableModel和QAbstractItemModel的直接子类中实现:
columnCount():
提供模型公开的数据列数。列表模型不提供此功能,因为它已在QAbstractListModel中实现。

3.1.2Editable items

可编辑模型允许修改数据项,还可以提供允许插入和删除行和列的功能。要实现编辑,必须正确实现以下功能:

flags():
必须为每个项目返回适当的标志组合。特别是,这个函数返回的值必须包括Qt::ItemIsEditable以及应用于只读模型中项目的值。

setData() :
用于修改与指定模型索引关联的数据项。为了能够接受用户界面元素提供的用户输入,此函数必须处理与Qt::EditRole关联的数据。该实现还可以接受与Qt::ItemDataRole指定的许多不同类型的角色相关联的数据。更改数据项后,模型必须发出dataChanged () 信号以通知其他组件更改

setHeaderData():
用于修改水平和垂直标题信息。更改数据项后,模型必须发出headerDataChanged () 信号以通知其他组件更改。

3.1.3可伸缩models

所有类型的模型都可以支持行的插入和删除,表模型和分层模型也可以支持列的插入和删除。表模型和层次模型也可以支持插入和删除列。将模型维度的变化在其发生之前与之后通知其他组件是非常重要的。因此,可以实现以下函数来允许调整模型的大小,但实现时必须确保调用适当的函数来通知附加的视图和委托。

insertRows():
用于向所有类型的模型添加新的数据行和数据项。实现必须在将新行插入任何底层数据结构前调用beginInsertRows (),完成后立即调用endInsertRows ()。

removeRows() :
用于从所有类型的模型中删除行及其包含的数据项。实现必须在从任何底层数据结构中删除行之前调用beginRemoveRows (),完成后立即调用endRemoveRows ()。

insertColumns():
用于将新列和数据项添加到表模型和分层模型中。实现必须调用在将新列插入任何底层数据结构前beginInsertColumns (),完成后立即调用endInsertColumns ()。

removeColumns():
用于从表模型和分层模型中删除列及其包含的数据项。实现必须在从任何底层数据结构中删除列前调用beginRemoveColumns (),完成后立即调用endRemoveColumns ()。

一般来说,如果操作成功,这些函数应该返回true。但是,可能会出现操作只部分成功的情况;例如,如果可以插入的行数少于指定的行数。在这种情况下,模型应该返回false,以表示未能使任何附加组件处理这种情况。

在调整大小的API的实现中调用的函数发出的信号使附加的组件有机会在任何数据变得不可用之前采取措施。使用begin和end函数对insert和remove操作进行封装也使模型能够正确管理持久性模型索引。

通常情况下,开始和结束函数能够将模型底层结构的变化告知其他组件。对于模型结构的更复杂的变化,可能涉及内部重组、数据排序或任何其他结构变化,有必要执行以下顺序:

  • 发出layoutAboutToBeChanged () 信号
  • 更新代表模型结构的内部数据。
  • 使用changePersistentIndexList ()更新持久索引
  • 发出layoutChanged () 信号。
    这个序列可以用于任何结构更新,以代替更高级更方便的保护方法。例如,如果一个有200万行的模型需要删除所有奇数行,那就是100万个折现范围,每个范围有1个元素。可以使用beginRemoveRows和endRemoveRows 100万次,但这显然是低效的。相反,这可以作为一个单一的布局变化信号,一次性更新所有必要的持久化索引。

3.1.4模型数据的懒惰人口

模型数据的懒惰人口有效地允许对模型信息的请求推迟到视图真正需要的时候。

有些模型需要从远程来源获取数据,或者必须执行耗时的操作来获取数据组织方式的信息。由于视图为了准确地显示模型数据,一般会请求尽可能多的信息,因此限制返回给它们的信息量,以减少不必要的后续数据请求,是很有用的。

在查找给定项的子项的数量是一项昂贵的操作的分层模型中,确保仅在必要时调用模型的rowCount () 实现很有用。在这种情况下,可以重新实现hasChildren () 函数,以便为视图提供一种廉价的方式来检查子项的存在,并且在QTreeView的情况下,为它们的父项绘制适当的装饰。

不管hasChildren () 的重新实现返回 true 还是 false ,视图可能不需要调用rowCount () 来找出有多少孩子存在。例如,如果父项没有展开以显示它们, QTreeView不需要知道有多少子项。

如果知道许多项目都会有孩子,那么重新实现hasChildren () 以无条件地返回 true 有时是一种有用的方法。这确保了以后可以检查每个项目的子项,同时尽可能快地进行模型数据的初始填充。唯一的缺点是在用户尝试查看不存在的子项之前,某些视图中可能会错误地显示没有子项的项。

3.2导航和索引创建

层次模型需要提供一些函数,让视图可以调用这些函数来导航它们所暴露的树状结构,并获取项目的模型索引。

3.2.1父与子

由于暴露给视图的结构是由底层数据结构决定的,所以每个模型子类都要通过提供以下函数的实现来创建自己的模型索引。

index() :
给定一个父项的模型索引,该函数允许视图和委托人访问该项的子项。如果找不到有效的子项–对应指定的行、列和父模型索引,函数必须返回QModelIndex(),这是一个无效的模型索引。

parent():
提供与任何给定子项的父项相对应的模型索引,如果指定的模型索引与模型中的顶层项相对应,或者在模型中没有有效的父项,则函数必须返回无效的模型索引,该索引是用空的QModelIndex()创建的。如果指定的模型索引对应于模型中的顶层项,或者如果模型中没有有效的父项,函数必须返回一个无效的模型索引,该索引由空的QModelIndex()构造函数创建。

上述两个函数都使用createIndex () 工厂函数来生成索引供其他组件使用。模型通常会为这个函数提供一些唯一的标识符,以确保模型索引以后可以与其对应的项目重新关联。

3.3拖放支持和MIME类型处理

模型/视图类支持拖放操作,提供了对许多应用程序来说足够的默认行为。然而,也可以自定义项目在拖放操作期间的编码方式,默认情况下是否复制或移动,以及如何将其插入现有模型中。

此外,便利视图类实现了专门的行为,这些行为应该严格遵循现有开发人员的预期。便利视图部分提供了此行为的概述。

3.3.1MIME data

默认情况下,内置模型和视图使用内部MIME类型( application/x-qabstractitemmodeldatalist )来传递有关模型索引的信息。这将指定项目列表的数据,其中包含每个项目的行号和列号,以及有关每个项目支持的角色的信息。

使用此 MIME 类型编码的数据可以通过调用QAbstractItemModel::mimeData ()来获得,其中QModelIndexList包含要序列化的项目。

在自定义模型中实现拖放支持时,可以通过重新实现以下函数,以特殊格式导出数据项。

mimeData() :
可以重新实现此功能,以返回默认 application/x-qabstractitemmodeldatalist 内部MIME类型以外的格式的数据。子类可以从基类获取默认的QMimeData对象,并以其他格式向其添加数据。

对于许多模型,以MIME类型表示的通用格式(例如 text/plain 和 image/png )提供项目的内容很有用。请注意,可以使用QMimeData :: setImageData(),QMimeData :: setColorData()和QMimeData :: setHtml()函数轻松地将图像,颜色和HTML文档添加到QMimeData对象。

3.3.2接受Drop的数据

在视图上执行拖放操作时,会查询底层模型以确定它支持哪些类型的操作以及它可以接受的 MIME 类型。此信息由QAbstractItemModel::supportedDropActions () 和QAbstractItemModel::mimeTypes () 函数提供。不覆盖QAbstractItemModel提供的实现的模型支持复制操作和项目的默认内部 MIME 类型。

当序列化的项目数据被拖放到视图上时,数据会使用QAbstractItemModel::dropMimeData () 的实现插入到当前模型中。该函数的默认实现永远不会覆盖模型中的任何数据;相反,它尝试将数据项插入为某项的同级项或该项的子项。

要利用QAbstractItemModel的内置MIME类型的默认实现,新模型必须提供以下功能的重新实现:

insertRows():
这些函数使模型能够使用QAbstractItemModel::dropMimeData ()提供的现有实现自动插入新数据。

insertColumns()

setData() :
允许新的行和列填充项目。

setItemData():
该功能为填充新项目提供了更有效的支持。

要接受其他形式的数据,必须重新实现这些函数。

supportedDropActions():
用于返回拖放操作的组合,表示模型接受的拖放操作的类型。

mimeTypes():
用于返回一个可以被模型解码和处理的MIME类型列表。一般来说,支持输入模型的MIME类型与模型在编码数据时可以使用的MIME类型相同,供外部组件使用。

dropMimeData():
对通过拖放操作传输的数据进行实际解码,确定其在模型中的位置,并在必要时插入新的行和列。该函数在子类中如何实现取决于每个模型所暴露的数据的要求。

如果dropMimeData () 函数的实现通过插入或删除行或列来更改模型的维度,或者如果修改了数据项,则必须注意确保发出所有相关信号。简单地调用子类中其他函数的重新实现(例如setData ()、insertRows () 和insertColumns ())会很有用,以确保模型的行为一致。

为了确保拖动操作正常工作,必须重新实现以下从模型中删除数据的函数。

  • removeRows()
  • removeRow()
  • removeColumns()
  • removeColumn()
    有关使用项目视图进行拖放的更多信息,请参阅对项目视图使用拖放。

3.3.3Convenience views

方便的视图(QListWidget、QTableWidget和QTreeWidget)覆盖默认的拖放功能以提供不太灵活但更自然的行为,适用于许多应用程序。例如,由于将数据放入QTableWidget的单元格中更为常见,用正在传输的数据替换现有内容,底层模型将设置目标项的数据,而不是将新行和列插入到模型中。有关在便捷视图中拖放的更多信息,您可以参阅在项目视图中使用拖放。

3.4大量数据的性能优化

canFetchMore () 函数检查父级是否有更多可用数据并相应地返回真 true 假。fetchMore () 函数根据指定的父对象获取数据。这两个函数可以组合在一起,例如,在涉及增量数据的数据库查询中以填充QAbstractItemModel。我们重新实现canFetchMore () 以指示是否有更多数据要获取,并根据需要重新实现fetchMore ( ) 来填充模型。

另一个例子是动态填充的树模型,当树模型中的一个分支展开时,我们重新实现fetchMore ()。

如果您重新实现fetchMore () 将行添加到模型中,则需要调用beginInsertRows () 和endInsertRows ()。此外,canFetchMore () 和fetchMore () 都必须重新实现,因为它们的默认实现返回 false 并且什么也不做。

4.参考资料

【1】《Qt Creator快速入门 第3版》.霍亚飞
【2】 QTreeView使用系列教程.百里杨
【3】 Qt Model/View教程.FlyWM_
【4】 Qt 6.2 [中文]Qt Widgets Model/View Programming.Runebook.dev
【5】 Qt助手

猜你喜欢

转载自blog.csdn.net/langshuibaren/article/details/131277292
今日推荐