MVC中的M就是模型Model,QT中所有的模型都继承自:QAbstractItemModel
查阅手册可知,它的子类有3个:
分别是列表模型、代理模型、表格模型。
要实现自定义模型,可以继承QAbstractItemModel以及任何一个后代类(含子类、孙子类。。。等),按照QT的设计惯例,名字里带抽象字样Abstract的类,都不能直接使用,必须继承并自行实现某些特定函数;对于QT自带的不带Abstract字样的Model类,可以不用继承重写,直接new出来使用即可,这种情形较为简单。所以本文只演示继承重写Abstract的情形,只要掌握了这种情形,那么不带Abstract的情形就更不在话下了。
以QAbstractTableModel为例,想使用它,就必须先看手册:
以上就是其标准用法了,这里再简单描述一下:QAbstractTableModel提供了二维数据模型的标准接口,可用于列表视图(不推荐)或者表格视图(推荐),虽然表格模型与列表视图是可以绑定的,但更好的方式继承列表模型QAbstractListModel。
子类化基本模型QAbstractItemModel时,我们继承后必须至少实现5个函数(见上述英文文档):index(), parent(), rowCount(), columnCount() data()。然而,子类化抽象表格模型QAbstractTableModel时,至少只需实现3个即可:rowCount(), columnCount(), and data(),因为抽象表格模型QAbstractTableModel,已经帮我们实现了两个函数index(), parent(),显然这比直接继承QAbstractItemModel来自定义模型要轻松一丁点。一般来说,最好把表头数据也重写一下:headerData()。
以上工作可以满足只读模型的最低需求了,如果想要该模型支持读写,那么还必须实现一下setData()和flags()这两个。
下面我们来实现一下rowCount()、columnCount()、data() 、headerData()、 setData()、flags()这6个函数,以实现完整支持读写的自定义模型。
这几个函数应该怎么写:
-
[pure virtual] int QAbstractItemModel::rowCount(const QModelIndex &parent = QModelIndex()) const
功能:返回指定索引下的行数,也即返回该索引有几个儿子。索引的行数这个概念,对于列表、表格、树,定义是不同的,可参考我的本系列教程另一篇博文《QModelIndex详解》
参数:parent为指定的索引
[pure virtual] int QAbstractItemModel::columnCount(const QModelIndex &parent = QModelIndex()) const
功能: 返回指定索引下的列数- data(index,role)
功能:读取指定index处的role类型的数据。一般View在渲染时,会自动调用该函数来获取数据。
说明:数据类型要根据role做转换,例如role=Qt::DisplayRole时,应当返回QString(返回时QT还会自动再将QString再次转换为QVariant)。其余role应当返回什么类型,请参考我的本系列教程另一篇博文《角色role的使用》 - headerData(n,orient,role) 返回表头第n个名字,到底要返回行表头还是列表头,取决于orient参数
- bool QAbstractItemModel::setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole)
功能:将index处的内容修改保存下来,并返回是否能修改成功。一般视图编辑完成后后自动调用该函数,以实现数据保存。
说明:视图送进来的数据,是用QVariant包装过的,我们需要把这个QVariant转换为Model可以保存的实际数据类型。 - flags(index)返回index处的数据特性:是否使能、是否允许编辑、是否允许选中等等
这里再次特意强调一点,Model只是用来管理数据Data的映射,真正的Data不一定非得处于Model内部(成员变量),Data可以是txt文件、数据库文件、操作系统内核数据(如系统文件目录)。不过为了便于展示自定义model的原理,我就把Data放在Model内部了,以成员变量的形式来存储Data。
为方便读者理解以下代码,这里先描述一下该代码的功能:MyTableModel中提供了一个表格数据模型,每一行是一个人的信息:名字、出生日期,共3条记录,也即这是一个3行2列的数据模型。
而且,通过该例读者可以发现,表格模型,并不一定要求原始数据真的是二维表,只要Model能够把原始数据映射成二维表即可,在本例中,每一列数据都是一个QStringList,根本不是二维结构,但本类通过重写data(index,role)函数,把这些不是二维结构的数据,映射成了二维,详情可阅读以下代码。
#include <QObject>
#include <QAbstractTableModel>
#include <QStringList>
class MyTableModel : public QAbstractTableModel
{
public:
MyTableModel(QObject *parent = 0);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;//读取index处的数据
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
Qt::ItemFlags flags(const QModelIndex &index) const;
private:
QStringList name;//第1列的内容
QStringList birth;//第2列的内容
QStringList headName;
};
#include "myTableModel.h"
#include <QDebug>
MyTableModel::MyTableModel(QObject *parent)
: QAbstractTableModel(parent)
{
name << "zhangSan" << "liSi" << "wangWu";
birth << "1970.01.01" << "1980.02.02" << "1990.03.03";
headName << "name" << "birth";
}
int MyTableModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return name.size();
}
int MyTableModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 2;
}
QVariant MyTableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation != Qt::Horizontal)
return QVariant("only support horizontal");
if(role != Qt::DisplayRole)
return QVariant();
if(section >= headName.size())
return QVariant("NoName");
return headName.at(section);
}
bool MyTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
Q_UNUSED(role)
int col = index.column();
int row = index.row();
if(0 == col)
{
name[row] = value.toString();
}
if(1 == col)
{
birth[row] = value.toString();
}
qDebug() << name << birth;
//数据已写完,下面通知视图类对象来更新界面
const QVector<int> roles;//哪些角色被更新
roles << role;
emit dataChanged(index, index, roles);//通知view哪些范围内的哪些角色数据被更新了
return true;
}
Qt::ItemFlags MyTableModel::flags(const QModelIndex &index) const
{
if(index.row() > 3 || index.column() > 2)
return Qt::NoItemFlags;
if(0 == index.column())
{
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
if(1 == index.column())
{
return Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable;
}
return Qt::NoItemFlags;
}
QVariant MyTableModel::data(const QModelIndex &index, int role) const
{
if(!index.isValid())
return QVariant();
//以下两种代码的区别,参见代码后面的动图
//代码1:只返回展示数据。这时用户双击单元格时,单元格会变空,交互体验很差。
//if(role != Qt::DisplayRole)
//代码2:展示数据和编辑数据均返回Model的当前值。这时用户双击单元格,会显示model的当前值
if(role != Qt::DisplayRole && role != Qt::EditRole)
return QVariant();
int row = index.row();
int col = index.column();
if(row >= name.size() || col >= 3)
return QVariant("indexError");
if(0 == col)
{
return name.at(row);
}
if(1 == col)
{
return birth.at(row);
}
return QVariant("indexError");
}
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
MyTableModel *model = new MyTableModel(this);
ui->tableView->setModel(model);
}
运行效果如下:而且由于我们在上述代码的flags(index)函数中没有给第1列设置“可选中”标志,所以用鼠标点击第1列是无法选中的(不会变蓝色),而点击第2列是可以选中并进入编辑状态的。
下图1是没有给EditRole返回空值,一旦双击进入编辑态,单元格就被清空了,图2是给EditRole返回当前Model的值。
下面再继续做一些测试,
因为在本例中我们的有效的真实数据的列数只有2列,如果我们把上述代码的列数函数columnCount()强制返回3,让行数强制返回5,会有什么效果?
先看之前的代码,视图打算显示第3列数据时,会通过前文的data(index)函数向模型索要数据,在这个函数中,我写的是:所请求的行或列索引超出了合法范围,则返回: QVariant("indexError");
对于列表头数据,我写的是:如果index不合法则返回: QVariant("NoName")。
对于数据的flag,我写的是:如果index不合法则返回: 无效标志。
看看运行效果:
int MyTableModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 5;
//return name.size();//正常
}
int MyTableModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 3;
//return 2;//正常
}
运行效果如下,与前文的分析完全一致。