0. 前言
可算到了这一篇了!
上一篇文章中把Qt::ItemDataRole
介绍完了,有了前面2篇文章的铺垫,现在终于能够开始动手实现自己的Model
类了。
我们在这篇文章中提到Model
相关类主要有QAbstractItemModel
、QAbstractListModel
、QAbstractTableModel
这三个,由于我们已经学习过QModelIndex
了,实现的难度区别不大,本文就选择从QAbstractTableModel
派生自己的Model
类。
话不多说,直奔主题。
系列文章回顾:
Qt Model/View 学习(1) - 是什么和为什么?
Qt Model/View 学习(2) - QModelIndex索引模型数据
Qt Model/View 学习(3) - 索引来一堆东西,究竟取谁(ItemDataRole)?
1. Data设计
为了(搞节目效果 )强调Data
确实可以很散漫,设计如下数据结构:
// 数据Data,共20个数据
// 10个标题
QStringList titles = {"飞机重量", "经度", "纬度", "高度", "飞行次数",
"城市", "宠物名称", "姓名", "身高", "体重"};
// 10个数据
struct GPS
{
// 经纬高
double lng=108.11, lat=37.11, alt=500.11;
};
struct Pilot
{
QString name="Tom"; // 姓名
QString city="北京"; // 城市
double stature=180.0; // 身高
double weight=66.5; // 体重
unsigned flyTimes=52; // 飞行次数
};
GPS pos; // GPS对象
Pilot person; // 飞行员对象
QString petName="Jerry"; // 宠物名称
double planeWeight=10000; // 飞机重量
总共20个数据,有QString、double、unsigned
等各种类型。在我们的Model
类中将被解析为4*5
的表格。
此处为了便利就直接将数据放到
Model
类的私有区了,在实际程序设计中可以考虑将变量整体封装成类/结构,然后将封装的对象指针传入Model
类中,这样可实现将Model
和Data
分开。
2. Model类设计
2.1 数据显示与对齐、字体修改
先找到QAbstractTableModel
中关于Subclassing
的部分:
官方文档告诉我们:必须重新实现rowCount()、columnCount()、data()
函数。
那我们从QAbstractTableModel
派生出自己的MyTableModel
类,简简单单实现一下这几个函数。如果对于QModelIndex
和Qt::DisplayRole
还不太熟悉,那就快去看看之前的文章吧~
virtual int rowCount(const QModelIndex &/*parent*/ = QModelIndex()) const override
{
// 解析为4*5矩阵,行数为4
return 4;
}
virtual int columnCount(const QModelIndex &/*parent*/ = QModelIndex()) const override
{
// 解析为4*5矩阵,列数为5
return 5;
}
// 核心函数,View类从Model中取数据,传入的参数在之前的文章中有介绍
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
{
switch(role)
{
// 显示数据用
case Qt::DisplayRole:
return getTableDisplayData(index);
break;
// 其余不处理
default:
return QVariant();
break;
}
}
其中的getTableDisplayData()
函数声明如下,由于只是将数据按矩阵格式对应了一下,用了一堆if-else-switch
就不贴全源代码了……
QVariant getTableDisplayData(const QModelIndex& index) const
{
/* 暂定显示形式为:第1行和第3行显示标题,第2行和第4行显示数值
* 姓名 身高 体重 城市 宠物名称
*
* 飞机重量 飞行次数 经度 纬度 高度
*
*/
// 稍作判断
if(!index.isValid() || index.column() >= columnCount() || index.row() >= rowCount())
return QVariant();
// 数据模型映射
// 第一行显示:姓名 身高 体重 城市 宠物名称
if(index.row()==0)
{
//titles = {"飞机重量", "经度", "纬度", "高度", "飞行次数", "城市", "宠物名称", "姓名", "身高", "体重"};
switch(index.column())
{
case 0: return titles[7];
break;
case 1: return titles[8];
break;
case 2: return titles[9];
break;
case 3: return titles[5];
break;
case 4: return titles[6];
break;
default: return QVariant();
break;
}
}
//后续类似......
}
在main()
函数中添加以下代码以使用咱们的MyTableModel
类:
// 使用TableView控件
QTableView tbl;
// 声明自己的Model对象
MyTableModel model;
// 关键接口:设置View对应的Model
tbl.setModel(&model);
tbl.resize(600,200);
tbl.show();
Ctrl+R
运行起来看看效果:
显示数据问题不大,但作为一名轻度强迫症,还想实现几点显示效果:
- 数据都居中显示;
- 第1行和第3行作为标题行,字体加粗一下;
如果有好好看上一篇关于ItemDataRole
的文章,这两条应该是很容易实现的,只需要在data()
函数中添加对齐和字体的Role
处理代码如下:
// 核心函数,View类从Model中取数据,传入的参数在之前的文章中有介绍
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
switch(role)
{
// 显示数据用
case Qt::DisplayRole:
return getTableDisplayData(index);
break;
// 对齐处理
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
break;
// 字体处理
case Qt::FontRole:
{
QFont font;
// row()==0,2时为标题行,需要加粗
if(index.row()%2==0) font.setBold(true);
return font;
}
break;
// 其余不处理
default:
return QVariant();
break;
}
}
这样看起来就稍微舒服一些了:
但实话说,还是很难看,个人还会想要达到以下效果:
- 把行列的表头给去了;
- 每列的宽度设置为刚刚好;
- 窗口大小刚好放下表格,而不像现在显示不全;
不过遗憾地,这几条都不是在MyTableModel
中能够处理的,而是对QTableView
进行操作。避免跑题,此处就不贴代码了,只提思路,具体代码可以参考附件。
QTableView::horizontalHeader()
获取水平表头,调用QHeaderView::hide()
函数将其隐藏;垂直表头处理类似;QTableView::resizeColumnsToContents()
函数,自适应列宽;- 计算自适应之后的表格总大小,使用
QTableView::resize()
函数设置控件大小,再使用QTableView::setHorizontalScrollBarPolicy()
隐藏水平滚动条,类似地,垂直滚动条也隐藏;
View
类将在下一篇文章进行系统讲解。
最后显示的结果如下,比较符合强迫症审美了~
2.2 数据修改
继续看QAbstractTableModel
中关于Subclassing
的部分:
提到:可编辑的模型需要实现setData()
和flags()
函数,后者返回值需包含Qt::ItemIsEditable
。
看起来都不难,那就继续简单实现一下:
// 可编辑模型需要实现flags(),返回值包含Qt::ItemIsEditable
virtual Qt::ItemFlags flags(const QModelIndex &index) const override
{
return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}
// 编辑模型的重点函数
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override
{
switch(role)
{
case Qt::EditRole:
// 修改成功时需要触发dataChanged()信号,返回true;修改失败返回false
// 第0、2行为标题行,不允许修改
if(!index.isValid() || index.row()%2==0) return false;
return editTableData(index, value);
break;
default:
return false;
break;
}
}
其中editTableData()
函数和getTableDisplayData()
类似,为了完成这样稀碎的数据的映射用了一堆switch-case
比较无聊,此处只贴上部分源代码:
bool editTableData(const QModelIndex &index, const QVariant &value)
{
// 根据传入index,修改表格上对应位置的数据
// 稍作判断
if(!index.isValid() || index.column() >= columnCount() || index.row() >= rowCount()) return false;
// 修改结果
bool ret = true;
switch(index.row())
{
// 只处理row()==1,3
// 姓名 身高 体重 城市 宠物名称
case 1:
switch(index.column())
{
case 0:
person.name = value.toString();
break;
case 1:
person.stature = value.toDouble();
break;
case 2:
person.weight = value.toDouble();
break;
case 3:
person.city = value.toString();
break;
case 4:
petName = value.toString();
break;
// 其余列未修改
default:
ret = false;
break;
}
break;
//此处省略case 3的修改
default:
ret = false;
break;
}
// 修改成功则触发信号
if(ret) emit dataChanged(index, index);
return ret;
}
Ctrl+R
运行试一下,已经可以修改内容了:
3. 小结
- 设计了一堆稀碎的原始数据,说实话添加了很多编码麻烦,但是在Model中正确处理后,都没啥问题;所以在设计数据结构的时候,最好能有一定的解析规律;
- 从
QAbstractTableModel
派生出自己的MyTableModel
类,通过实现data()、rowCount()、columnCount()
接口,完成数据的显示; - 实现
flags()、setData()
接口后,完成表格编辑功能; - 使用
QAbstractItemView::setModel()
接口,将自定义的Model
对象指针传入,完成Model
与View
的联动; - 项目的源代码在此处下载。
如有错误欢迎指正,共同进步~
今天你学废了吗?