首页 > 其他分享 >深入理解模型视图、自定义模型

深入理解模型视图、自定义模型

时间:2023-10-10 10:07:08浏览次数:27  
标签:index Qt 自定义 模型 视图 数据 QModelIndex


一、深入理解模型

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

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


深入理解模型视图、自定义模型_模型视图_02


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

    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);


深入理解模型视图、自定义模型_模型视图_03

    图中,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);


通过 parent 属性区别开来。

数据角色。模型可以针对不同的组件(或者组件的不同部分,比如按钮的提示以及显示的文本等)提供不同的数据。例如,Qt::DisplayRole用于视图的文本显示。通常来说,数据项包含一系列不同的数据角色,这些角色定义在Qt::ItemDataRole枚举中。

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


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


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

    总结一下:

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

    另外


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

二、深入理解视图


数据通过视图向用户进行显示。此时,这种显示方式不必须同模型的存储结构相一致。实际上,很多情况下,数据的显示同底层数据的存储是完全不同的。

    我们使用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);
}


深入理解模型视图、自定义模型_QT_04


三、自定义模型


    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(){
}


深入理解模型视图、自定义模型_模型视图_05


标签:index,Qt,自定义,模型,视图,数据,QModelIndex
From: https://blog.51cto.com/u_6526235/7787941

相关文章

  • JVM之内存模型
    前言首先说明下JVM内存模型和Java内存模型这是两个不同的概念,不要搞混淆了。JVM内存模型定义了Java程序在运行时如何分配、使用和释放内存,跟存储和执行相关,也就是常说的运行时数据区域。Java内存模型(JavaMemoryModel,简称JMM)是一种规范,定义了线程和主内存之间的抽象关系,所有......
  • 《动手学深度学习 Pytorch版》 8.3 语言模型和数据集
    8.3.1学习语言模型依靠在8.1节中对序列模型的分析,可以在单词级别对文本数据进行词元化。基本概率规则如下:\[P(x_1,x_2,\dots,x_T)=\prod^T_{t=1}P(x_t|x_1,\dots,x_{t-1})\]例如,包含了四个单词的一个文本序列的概率是:\[P(deep,learning,is,fun)=P(deep)P(learning|deep)P(i......
  • 【Angular】视图封装
    参考:WhatisViewEncapsulationinAngular|ViewEncapsulation作用:控制样式作用域类型:ViewEncapsulation.Emulated(默认值),本组件的样式只会应用到自身,不会应用到自己的子组件中,也不会流出到父组件。ViewEncapsulation.None,本组件的样式向外流出,污染全局,所有组件的样式都会......
  • 注解(Annotation)简介,内置注解,元注解,自定义注解
    注解:不是程序本身,可以对程序做出解释(这一点和注释没什么区别)可以被其他程序(比如编译器)读取@Functional(这是L)(这是I)Interface  函数式接口 内置注解:java本来就有的注解@Override  重写@Deprecated 已废弃,不推荐使用的意思@SuppressWarnings 镇压警告 ......
  • CSS盒子模型
    对html进行封装:包括外边距、边框、内边距和实际内容<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title......
  • 全域Serverless+AI,华为云加速大模型应用开发
    日前,华为全联接大会2023在上海召开。华为云CTO张宇昕在大会上发布了基于Serverless技术的大模型应用开发框架,框架以面向AI领域全新升级的FunctionGraph3.0为核心,将BaaSforAI后端和开放平台快速无缝集成,助力企业轻松商用AI应用。在“全域Serverless+AI加速应用创新”专题......
  • 造轮子之自定义授权策略
    前面我们已经弄好了用户角色这块内容,接下来就是我们的授权策略。在asp.netcore中提供了自定义的授权策略方案,我们可以按照需求自定义我们的权限过滤。这里我的想法是,不需要在每个Controller或者Action打上AuthorizeAttribute,自动根据ControllerName和ActionName匹配授权。只需要......
  • 自定义滚动条 css
    /*自定义滚动条css*/.customScrollbar::-webkit-scrollbar{width:10px;height:10px;}.customScrollbar::-webkit-scrollbar-thumb{border-radius:8px;background-color:#47515b;}.customScrollbar::-webkit-scrollbar-thumb:hover{background-color:#5D626C;}.customScr......
  • 什么是 模型推理
    人工只能的黑话:推理。 网上查了一下,其实就是预测的意思,就是在计算因变量模型的训练指的是计算公式里面的参数。  这个图,很明白的说明了 推理的含义。 但是inference翻译成推理,让我琢磨了好半天。......
  • R语言ARMA-GARCH模型金融产品价格实证分析黄金价格时间序列
    全文链接:http://tecdat.cn/?p=32677原文出处:拓端数据部落公众号最近我们被客户要求撰写关于ARMA-GARCH的研究报告,包括一些图形和统计输出。研究黄金价格的动态演变过程至关重要。文中以黄金交易市场下午定盘价格为基础,帮助客户利用时间序列的相关理论,建立了黄金价格的ARMA-GA......