一、深入理解模型

在 model/view 架构中,model 提供一种标准接口,供视图和委托访问数据。在 Qt 中,这个接口由QAbstractItemModel类进行定义。不管底层数据是如何存储的,只要是QAbstractItemModel的子类,都提供一种表格形式的层次结构。视图利用统一的转换来访问模型中的数据。但是,需要提供的是,尽管模型内部是这样组织数据的,但是并不要求也得这样子向用户展示数据。
    下面是各种 model 的组织示意图。我们利用此图来理解什么叫“一种表格形式的层次结构”。

如上图所示,List Model 虽然是线性的列表,也有一个 Root Item(根节点),之下才是呈线性的一个个数据,而这些数据实际可以看作是一个只有一列的表格,但是它是有层次的,因为有一个根节点。Table Model 就比较容易理解,只是也存在一个根节点。Tree Model 主要面向层次数据,而每一层次都可以都很多列,因此也是一个带有层次的表格。

为了能够使得数据的显示同存储分离,我们引入模型索引(model index)的概念。通过索引,我们可以访问模型的特定元素的特定部分。视图和委托使用索引来请求所需要的数据。由此可以看出,只有模型自己需要知道如何获得数据,模型所管理的数据类型可以使用通用的方式进行定义。索引保存有创建的它的那个模型的指针,这使得同时操作多个模型成为可能。

QAbstractItemModel *model = index.model();

模型索引提供了所需要的信息的临时索引,可以用于通过模型取回或者修改数据。由于模型随时可能重新组织其内部的结构,因此模型索引很可能变成不可用的,此时,就不应该保存这些数据。如果你需要长期有效的数据片段,必须创建持久索引。持久索引保证其引用的数据及时更新。临时索引(也就是通常使用的索引)由QModelIndex类提供,持久索引则是QPersistentModelIndex类。

为了定位模型中的数据,我们需要三个属性:行号、列号以及父索引。下面我们对其一一进行解释。

我们前面介绍过模型的基本形式:数据以二维表的形式进行存储。此时,一个数据可以由行号和列号进行定位。注意,我们仅仅是使用“二维表”这个名词,并不意味着模型内部真的是以二维数组的形式进行存储;所谓“行号”“列号”,也仅仅是为方便描述这种对应关系,并不真的是有行列之分。通过指定行号和列号,我们可以定位一个元素项,取出其信息。此时,我们获得的是一个索引对象(回忆一下,通过索引我们可以获取具体信息):

QModelIndex index = model->index(row, column, ...);

模型提供了一个简单的接口,用于列表以及表格这种非层次视图的数据获取。不过,正如上面的代码暗示的那样,实际接口并不是那么简单。我们可以通过文档查看这个函数的原型:

QModelIndex QAbstractItemModel::index(int row,int column,const QModelIndex &parent=QModelIndex()) const

这里,我们仅仅使用了前两个参数。通过下图来理解一下:

在一个简单的表格中,每一个项都可以由行号和列号确定。因此,我们只需提供两个参数即可获取到表格中的某一个数据项:

QModelIndex indexA = model->index(0, 0, QModelIndex());
    QModelIndex indexB = model->index(1, 1, QModelIndex());
    QModelIndex indexC = model->index(2, 1, QModelIndex());

函数的最后一个参数始终是 QModelIndex(),接下来我们就要讨论这个参数的含义。

在类似表格的视图中,比如列表和表格,行号和列号足以定位一个数据项。但是,对于树型结构,仅有两个参数就不足够了。这是因为树型结构是一个层次结构,而层次结构中每一个节点都有可能是另外一个表格。所以,每一个项需要指明其父节点。前面说过,在模型外部只能用过索引访问内部数据,因此,index()函数还需要一个 parent 参数:

QModelIndex index = model->index(row, column, parent);

图中,A 和 C 都是模型中的顶级项:

QModelIndex indexA = model->index(0, 0, QModelIndex());
    QModelIndex indexC = model->index(2, 1, QModelIndex());

A 还有自己的子项。那么,我们就应该使用下面的代码获取 B 的索引:

QModelIndex indexB = model->index(1, 0, indexA);

由此我们看到,如果只有行号和列号两个参数,B 的行号是 1,列号是 0,这同与 A 同级的行号是 1,列号是 0 的项相同,所以我们通过 parent 属性区别开来。

以上我们讨论了有关索引的定位。现在我们来看看模型的另外一个部分:数据角色。模型可以针对不同的组件(或者组件的不同部分,比如按钮的提示以及显示的文本等)提供不同的数据。例如,Qt::DisplayRole用于视图的文本显示。通常来说,数据项包含一系列不同的数据角色,这些角色定义在Qt::ItemDataRole枚举中。

我们可以通过指定索引以及角色来获得模型所提供的数据:

QVariant value = model->data(index, role);

通过为每一个角色提供恰当的数据,模型可以告诉视图和委托如何向用户显示内容。不同类型的视图可以选择忽略自己不需要的数据。当然,我们也可以添加我们所需要的额外数据。

总结一下:

  • 模型使用索引来提供给视图和委托有关数据项的位置的信息,这样做的好处是,模型之外的对象无需知道底层的数据存储方式;
  • 数据项通过行号、列号以及父项三个坐标进行定位;
  • 模型索引由模型在其它组件(视图和委托)请求时才会被创建;
  • 如果使用index()函数请求获得一个父项的可用索引,该索引会指向模型中这个父项下面的数据项。这个索引指向该项的一个子项;如果使用index()函数请求获得一个父项的不可用索引,该索引指向模型的最顶级项;
  • 角色用于区分数据项的不同类型的数据。

另外

  • 模型的数目信息可以通过rowCount()columnCount()获得。这些函数需要制定父项;
  • 索引用于访问模型中的数据。我们需要利用行号、列号以及父项三个参数来获得该索引;
  • 当我们使用QModelIndex()创建一个空索引使用时,我们获得的就是模型中最顶级项;
  • 数据项包含了不同角色的数据。为获取特定角色的数据,必须指定这个角色。

二、深入理解视图

前面我们介绍了模型的概念。下面则是另外一个基本元素:视图。在 model/view 架构中,视图是数据从模型到最终用户的途径。数据通过视图向用户进行显示。此时,这种显示方式不必须同模型的存储结构相一致。实际上,很多情况下,数据的显示同底层数据的存储是完全不同的。

我们使用QAbstractItemModel提供标准的模型接口,使用 QAbstractItemView提供标准的视图接口,而结合这两者,就可以将数据同表现层分离,在视图中利用前面所说的模型索引。视图管理来自模型的数据的布局:既可以直接渲染数据本身,也可以通过委托渲染和编辑数据。

视图不仅仅用于展示数据,还用于在数据项之间的导航以及数据项的选择。另外,视图也需要支持很多基本的用户界面的特性,例如右键菜单以及拖放。视图可以提供数据编辑功能,也可以将这种编辑功能交由某个委托完成。视图可以脱离模型创建,但是在其进行显示之前,必须存在一个模型。也就是说,视图的显示是完全基于模型的,这是不能脱离模型存在的。对于用户的选择,多个视图可以相互独立,也可以进行共享。

某些视图,例如QTableViewQTreeView,不仅显示数据,还会显示列头或者表头。这些是由QHeaderView视图类提供的,表头通常访问视图所包含的同一模型。它们使用QAbstractItemModel::headerData()函数从模型中获取数据,然后将其以标签 label 的形式显示出来。我们可以通过继承QHeaderView类,实现某些更特殊的功能。

#include "widget.h"
#include <QStringList>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSpinBox>
Widget::Widget(QWidget *parent): QWidget(parent)
{this->setWindowTitle("视图和委托");this->resize(300,300);QStringList data;data<<"0"<<"1"<<"2";model=new QStringListModel(this);model->setStringList(data);listview=new QListView(this);listview->setModel(model);btm=new QPushButton(tr("show model"),this);connect(btm,&QPushButton::clicked,this,&Widget::showmodel);QHBoxLayout *hl=new QHBoxLayout;hl->addWidget(btm);QVBoxLayout *vl=new QVBoxLayout;vl->addWidget(listview);vl->addLayout(hl);setLayout(vl);//将这个委托设置为QListView所使用的委托listview->setItemDelegate(new SpinBoxDelegate(listview));
}Widget::~Widget()
{}
void Widget::showmodel(){}
/** createEditor()返回一个组件。该组件会被作为用户编辑数据时所使用的编辑器,* 从模型中接受数据,返回用户修改的数据。在createEditor()函数中,* parent 参数会作为新的编辑器的父组件。
*/
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,const QStyleOptionViewItem & /* option */,const QModelIndex & /* index */) const
{QSpinBox *editor = new QSpinBox(parent);editor->setMinimum(0);editor->setMaximum(100);return editor;
}
/** setEditorData()函数从模型中获取需要编辑的数据(具有 Qt::EditRole 角色)。由于我们* 知道它就是一个整型,因此可以放心地调用 toInt()函数。 editor 就是所生成的编辑器实例,* 我们将其强制转换成 QSpinBox 实例,设置其数据作为默认值。
*/
void SpinBoxDelegate::setEditorData(QWidget *editor,const QModelIndex &index) const
{int value = index.model()->data(index, Qt::EditRole).toInt();QSpinBox *spinBox = static_cast<QSpinBox*>(editor);spinBox->setValue(value);
}
/** 在用户编辑完数据后,委托会调用setModelData()函数将新的数据保存到模型中。* 因此,在这里我们首先获取QSpinBox实例,得到用户输入值,然后设置到模型相应的位置。* 标准的QStyledItemDelegate类会在完成编辑时发出closeEditor()信号,视图会保证编辑* 器已经关闭,但是并不会销毁,因此需要另外对内存进行管理。由于我们的处理很简单,* 无需发出closeEditor()信号,但是在复杂的实现中,记得可以在这里发出这个信号。* 针对数据的任何操作都必须提交给QAbstractItemModel,这使得委托独立于特定的视图。* 当然,在真实应用中,我们需要检测用户的输入是否合法,是否能够存入模型。
*/
void SpinBoxDelegate::setModelData(QWidget *editor,QAbstractItemModel *model,const QModelIndex &index) const
{QSpinBox *spinBox = static_cast<QSpinBox*>(editor);spinBox->interpretText();int value = spinBox->value();model->setData(index, value, Qt::EditRole);
}
/** 最后,由于我们的编辑器只有一个数字输入框,所以只是简单将这个输入框的大小* 设置为单元格的大小(由option.rect提供)。如果是复杂的编辑器,我们需要根据* 单元格参数(由option提供)、数据(由index提供)结合编辑器(由editor提供)* 计算编辑器的显示位置和大小。
*/
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option,const QModelIndex &index) const
{editor->setGeometry(option.rect);
}


三、自定义模型

model/view 模型将数据与视图分割开来,也就是说,我们可以为不同的视图,QListViewQTableViewQTreeView提供一个数据模型,这样我们可以从不同角度来展示数据的方方面面。但是,面对变化万千的需求,Qt 预定义的几个模型是远远不能满足需要的。因此,我们还必须自定义模型。

类似QAbstractView类之于自定义视图,QAbstractItemModel 为自定义模型提供了一个足够灵活的接口。它能够支持数据源的层次结构,能够对数据进行增删改操作,还能够支持拖放。不过,有时候一个灵活的类往往显得过于复杂,所以,Qt 又提供了QAbstarctListModelQAbstractTableModel两个类来简化非层次数据模型的开发。顾名思义,这两个类更适合于结合列表和表格使用。

在开始自定义模型之前,我们首先需要思考这样一个问题:我们的数据结构适合于哪种视图的显示方式?是列表,还是表格,还是树?如果我们的数据仅仅用于列表或表格的显示,那么QAbstractListModel或者QAbstractTableModel 已经足够,它们为我们实现了很多默认函数。但是,如果我们的数据具有层次结构,并且必须向用户显示这种层次,我们只能选择QAbstractItemModel。不管底层数据结构是怎样的格式,最好都要直接考虑适应于标准的QAbstractItemModel的接口,这样就可以让更多视图能够轻松访问到这个模型。

现在,我们开始自定义一个模型。这个例子修改自《C++ GUI Programming with Qt4, 2nd Edition》。首先描述一下需求。我们想要实现的是一个货币汇率表,就像银行营业厅墙上挂着的那种电子公告牌。当然,你可以选择QTableWidget。的确,直接使用QTableWidget确实很方便。但是,试想一个包含了 100 种货币的汇率表。显然,这是一个二维表,并且对于每一种货币,都需要给出相对于其他 100 种货币的汇率(我们把自己对自己的汇率也包含在内,只不过这个汇率永远是 1.0000)。现在,按照我们的设计,这张表要有 100 x 100 = 10000 个数据项。我们希望减少存储空间,有没有更好的方式?于是我们想,如果我们的数据不是直接向用户显示的数据,而是这种货币相对于美元的汇率,那么其它货币的汇率都可以根据这个汇率计算出来了。比如,我存储人民币相对美元的汇率,日元相对美元的汇率,那么人民币相对日元的汇率只要作一下比就可以得到了。这种数据结构就没有必要存储 10000 个数据项,只要存储 100 个就够了(实际情况中这可能是不现实的,因为两次运算会带来更大的误差,但这不在我们现在的考虑范畴中)。

于是我们设计了CurrencyModel类。它底层使用QMap<QString, double>数据结构进行存储,QString类型的键是货币名字,double类型的值是这种货币相对美元的汇率。(这里提一点,实际应用中,永远不要使用 double 处理金额敏感的数据!因为 double 是不精确的,不过这一点显然不在我们的考虑中。)

#include "currencymodel.h"CurrencyModel::CurrencyModel(QObject *parent): QAbstractTableModel(parent){
}/** rowCount()和 columnCount()用于返回行和列的数目。
*/
int CurrencyModel::rowCount(const QModelIndex & parent) const{return currencyMap.count();
}int CurrencyModel::columnCount(const QModelIndex & parent) const{return currencyMap.count();
}
/** 这里我们首先判断这个角色是不是用于显示的,如果是,则调用 currencyAt()函数* 返回第 section 列的键值;如果不是则返回一个空白的QVariant 对象
*/
QVariant CurrencyModel::headerData(int section, Qt::Orientation, int role) const{if (role != Qt::DisplayRole) {return QVariant();}return currencyAt(section);
}
/** Qt 提供了 QVariant 类型,你可以把很多类型存放进去,到需要使用的时候使用一* 系列的 to 函数取出来即可。
*/
QString CurrencyModel::currencyAt(int offset) const{return (currencyMap.begin() + offset).key();
}
/** 我们当然可以直接设置 currencyMap,但是我们依然添加了 beginResetModel()和* endResetModel()两个函数调用。这将告诉关心这个模型的其它类,现在要重置内部* 数据,大家要做好准备。这是一种契约式的编程方式。
*/
void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map){beginResetModel();currencyMap = map;endResetModel();
}
/** data()函数返回一个单元格的数据。它有两个参数:第一个是 QModelIndex,也就是单元格* 的位置;第二个是 role,也就是这个数据的角色。这个函数的返回值是 QVariant 类型。* 我们首先判断传入的 index 是不是合法,如果不合法直接返回一个空白的 QVariant。然后* 如果 role 是 Qt::TextAlignmentRole,也就是文本的对齐方式,返回 int(Qt::AlignRight |* Qt::AlignVCenter);如果是 Qt::DisplayRole,就按照逻辑进行计算,然后以字符串的格式返回。
*/
QVariant CurrencyModel::data(const QModelIndex &index, int role) const{if (!index.isValid()) {return QVariant();}if (role == Qt::TextAlignmentRole) {return int(Qt::AlignRight | Qt::AlignVCenter);} else if (role == Qt::DisplayRole|| role == Qt::EditRole) {QString rowCurrency = currencyAt(index.row());QString columnCurrency = currencyAt(index.column());if (currencyMap.value(rowCurrency) == 0.0) {return "####";}double amount = currencyMap.value(columnCurrency)/ currencyMap.value(rowCurrency);return QString("%1").arg(amount, 0, 'f', 4);//用arg中的内容替换%1}return QVariant();
}
/** 在 Qt 的 model/view 模型中,我们使用委托 delegate 来实现数据的编辑。* 在实际创建编辑器之前,委托需要检测这个数据项是不是允许编辑。模型必须* 让委托知道这一点,这是通过返回模型中每个数据项的标记 flag 来实现的,* 也就是这个 flags() 函数。这本例中,只有行和列的索引不一致的时候,我们* 才允许修改(因为对角线上面的值恒为 1.0000,不应该对其进行修改)* 注意,我们并不是在判断了index.row() != index.column()之后直接返回* Qt::ItemIsEditable,而是返回QAbstractItemModel::flags(index) |* Qt::ItemIsEditable。这是因为我们不希望丢弃原来已经存在的那些标记。
*/
Qt::ItemFlags CurrencyModel::flags(const QModelIndex &index) const
{Qt::ItemFlags flags = QAbstractItemModel::flags(index);if (index.row() != index.column()) {flags |= Qt::ItemIsEditable;}return flags;
}
/** 当数据重新设置时,模型必须通知视图,数据发生了变化。这要求我们必须发出 dataChanged()信号。* 由于我们只有一个数据发生了改变,因此这个信号的两个参数是一致的(dataChanged()的两个参数是* 发生改变的数据区域的左上角和右下角的索引值,由于我们只改变了一个单元格,所以二者是相同的)。
*/
bool CurrencyModel::setData(const QModelIndex &index,const QVariant &value, int role){if (index.isValid()&& index.row() != index.column()&& role == Qt::EditRole) {QString columnCurrency = headerData(index.column(),Qt::Horizontal, Qt::DisplayRole).toString();QString rowCurrency = headerData(index.row(),Qt::Vertical, Qt::DisplayRole).toString();currencyMap.insert(columnCurrency,value.toDouble() * currencyMap.value(rowCurrency));emit dataChanged(index, index);return true;}return false;
}CurrencyModel::~CurrencyModel(){
}

深入理解模型视图、自定义模型相关推荐

  1. 2021-07-31-DJ-007 Django 模型,自定义模型管理器Manager,自定义字段验证器Validdator

    一.模型 管理器 模型实例通过模型管理器获得并传递给使用者,修改默认的模型管理器,可以提前进行一些筛选步骤,比如筛掉已经注销的用户.已经取消的订单等等. 默认的模型管理器是django.db.mode ...

  2. Qt中的模型视图设计模式

    文章目录 1 初探Qt中的模型视图设计模式 2 模型视图中的索引 3 模型中的数据组织方式初探 1 初探Qt中的模型视图设计模式 模型视图设计模式的核心思想: 模型(数据)与视图(显示)相分离. 模型 ...

  3. Qt模型视图中的委托

    文章目录 1 初探QT模型视图中的委托 2 自定义委托 3 深度解析视图与委托 3.1 实例1:自定义视图显示方式 3.2 实例2:自定义视图显示方式提升用户体验 1 初探QT模型视图中的委托 首先来 ...

  4. QT学习笔记14(Qt模型视图结构)

    一.基础知识 1.MVC设计模式:是一种与用户界面相关的设计模式,通过此模式,可以有效的分离数据和用户界面. MVC包括三个要素: 表示数据的模型(Model):应用程序的对象 表示用户界面的视图(V ...

  5. 3、计算机图形学——模型视图变换、投影变换与视口变换

    一.模型视图变换 模型视图变换主要是为了让摄像机回归到世界坐标的原点并且和拍摄物体一起进行变换,便于计算 模型视图变换的根据就是物体和相机的相对位置不变,那么,投影得到的图片也是不变的 首先规定相机拍 ...

  6. 【Qt】模型/视图结构和数据绑定控件

    模型/视图结构 模型/视图是指处理数据的模型和向用户提供GUI的视图,这是从GUI分离数据的一种结构,又称为MVC(Model-View-Controller). 模型 QAbstractItemMo ...

  7. [diango]理解django视图工作原理

    前言:正确理解django视图view,模型model,模板的概念及其之间的关联关系,才能快速学习并上手使用django制作网页 本文主要讲解自己在学习django后对视图view的理解 在进入正文之 ...

  8. keras保存模型_TF2 8.模型保存与加载

    举个例子:先训练出一个模型 import 接下来第一种方法:只保留模型的参数:这个有2种方法: model.save_weights("adasd.h5")model.load_w ...

  9. [转载]Qt之模型/视图(自定义风格)

    原文地址:Qt之模型/视图(自定义风格)作者:一去丶二三里 关于自定义风格是针对视图与委托而言的,使用事件与QSS都可以进行处理,今天关于美化的细节讲解一下. 先看下图: 先撇开界面的美观性(萝卜青菜 ...

最新文章

  1. Android 活动与活动间数据传递
  2. PHP HashTable总结
  3. 晶振波形不是正弦波_10天电子入门-电感/晶振/声音
  4. (转) Unity3D 使用Texturepacker打包工具制作NGUI(Atlas)图集
  5. C#---HTML 转文本及HTML内容提取
  6. 建议收藏 | 全面解析 50+条 SQL 语句性能优化策略
  7. 收藏!5V转3.3V电平的19种方法技巧
  8. ActiveMQ代码示例
  9. 方法二 、属性 CLR学习第九课
  10. poj2991 Crane
  11. iOS开发中设置UITextField的占位文字的颜色,和光标的颜色
  12. 解决Linux系统下磁盘IO紧张的一种方法
  13. java进销存系统--采购入库
  14. 刀塔霸业怎么在电脑上玩 刀塔霸业电脑版玩法教程
  15. 友好的可视化工具——trelliscope
  16. C#删除文件和文件夹到回收站
  17. 门头沟的《一出好戏》
  18. UVALive 4126 Password Suspects(AC自动机+dp)
  19. 曲线曲面的基本理论3之曲线的参数表示
  20. SAP中非限制到冻结库存的相互转换操作测试过程记录

热门文章

  1. 开源的容器虚拟化平台Docker学习笔记,个人私藏分享,不谢!
  2. 内连接(join、inner join )、左连接(left join) 、全连接(full join)
  3. Nginx转发https访问的实现
  4. 微x模块怎么导入主题_模块
  5. 第十四届蓝桥杯大赛软件赛省赛-试题 B---01 串的熵 解题思路+完整代码
  6. [Matlab]FIR滤波器设计:(基本窗函数FIR滤波器设计)
  7. mysql 分位数 知乎_分位数的意义是什么?
  8. 深圳市智汇机器人科技有限公司环宇智行
  9. python之pip常用命令
  10. [读书笔记]多线程学习笔记