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

一、深入理解模型

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

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值