一、深入理解模型
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);
通过 parent 属性区别开来。
数据角色。模型可以针对不同的组件(或者组件的不同部分,比如按钮的提示以及显示的文本等)提供不同的数据。例如,Qt::DisplayRole
用于视图的文本显示。通常来说,数据项包含一系列不同的数据角色,这些角色定义在Qt::ItemDataRole
枚举中。
指定索引以及角色来获得模型所提供的数据:
QVariant value = model->data(index, role);
通过为每一个角色提供恰当的数据,模型可以告诉视图和委托如何向用户显示内容。不同类型的视图可以选择忽略自己不需要的数据。当然,我们也可以添加我们所需要的额外数据。
总结一下:
- 模型使用索引来提供给视图和委托有关数据项的位置的信息,这样做的好处是,模型之外的对象无需知道底层的数据存储方式;
- 数据项通过行号、列号以及父项三个坐标进行定位;
- 模型索引由模型在其它组件(视图和委托)请求时才会被创建;
- 如果使用
index()
函数请求获得一个父项的可用索引,该索引会指向模型中这个父项下面的数据项。这个索引指向该项的一个子项;如果使用index()
函数请求获得一个父项的不可用索引,该索引指向模型的最顶级项; - 角色用于区分数据项的不同类型的数据。
另外
- 模型的数目信息可以通过
rowCount()
和columnCount()
获得。这些函数需要制定父项; - 索引用于访问模型中的数据。我们需要利用行号、列号以及父项三个参数来获得该索引;
- 当我们使用
QModelIndex()
创建一个空索引使用时,我们获得的就是模型中最顶级项; - 数据项包含了不同角色的数据。为获取特定角色的数据,必须指定这个角色。
二、深入理解视图
数据通过视图向用户进行显示。此时,这种显示方式不必须同模型的存储结构相一致。实际上,很多情况下,数据的显示同底层数据的存储是完全不同的。
我们使用QAbstractItemModel
提供标准的模型接口,使用 QAbstractItemView
提供标准的视图接口,而结合这两者,就可以将数据同表现层分离,在视图中利用前面所说的模型索引。视图管理来自模型的数据的布局:既可以直接渲染数据本身,也可以通过委托渲染和编辑数据。
视图不仅仅用于展示数据,还用于在数据项之间的导航以及数据项的选择。另外,视图也需要支持很多基本的用户界面的特性,例如右键菜单以及拖放。视图可以提供数据编辑功能,也可以将这种编辑功能交由某个委托完成。视图可以脱离模型创建,但是在其进行显示之前,必须存在一个模型。也就是说,视图的显示是完全基于模型的,这是不能脱离模型存在的。对于用户的选择,多个视图可以相互独立,也可以进行共享。
某些视图,例如QTableView
和QTreeView
,不仅显示数据,还会显示列头或者表头。这些是由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 模型将数据与视图分割开来,也就是说,我们可以为不同的视图,QListView
、QTableView
和QTreeView
提供一个数据模型,这样我们可以从不同角度来展示数据的方方面面。但是,面对变化万千的需求,Qt 预定义的几个模型是远远不能满足需要的。因此,我们还必须自定义模型。
类似QAbstractView
类之于自定义视图,QAbstractItemModel
为自定义模型提供了一个足够灵活的接口。它能够支持数据源的层次结构,能够对数据进行增删改操作,还能够支持拖放。不过,有时候一个灵活的类往往显得过于复杂,所以,Qt 又提供了QAbstarctListModel
和QAbstractTableModel
两个类来简化非层次数据模型的开发。顾名思义,这两个类更适合于结合列表和表格使用。
我们的数据结构适合于哪种视图的显示方式?是列表,还是表格,还是树?如果我们的数据仅仅用于列表或表格的显示,那么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(){
}