0. 前言
上一篇文章中介绍了如何从QAbstractItemModel
派生出自己的Model
类,实现在QTableView
上的数据显示和编辑功能。其中涉及到了一部分关于QTableView
的操作没有细说,本文就来趁热打铁讲一讲QTableView
的使用方法。
本文的标题中有(优雅)的字眼,是由于在学完官方文档后,还没写本文之前先查了一下现有的博客对于QTableView
的介绍,发现大部分的教程都在使用QStandardItemModel
以及QStandardItem
来与QTableView
联动,个人认为这种做法并不优雅(小声bb 👻)。
我们在这篇文章中,讲到有关的Model
类时提到了QStandardItemModel
,它提供了一种使用控件的Model
类,所用到的QStandardItem
类就是它支持的控件。它Model了,但没完全Model。这样一来为何不直接使用基于控件的QTableWidget
呢?
笔者认为,QTableView
已经是封装完备的一个类了,如果使用Model/View
框架,除了极少数特殊需求场合,基本不需要从QTableView
派生自己的View
类。
另外,本文虽介绍QTableView
,但更关键的代码或许还是在Model
类这边。
如果想直接看源码的话,可以跳转到此处。
系列文章回顾:
Qt Model/View 学习(1) - 是什么和为什么?
Qt Model/View 学习(2) - QModelIndex索引模型数据
Qt Model/View 学习(3) - 索引来一堆东西,究竟取谁(ItemDataRole)?
Qt Model/View 学习(4) - 实现自己的QAbstractTableModel类(支持显示与修改)
1. View家族
先来一张UML类图,看看QTableView
的家族渊源:
图中的三角形表示泛化关系,指向基类。更多关于UML类图的知识可以看这篇文章。
与Model
类相似地,View
类的C位也是一个Abstract
类:QAbstractItemView
。它继承了QAbstractScrollArea
表明它支持滚动条。
它派生出来一系列的类,而本文主角QTableView
则为其中之一,且QTableView
中还包含了QHeaderView
,这是表格的横纵标题栏对象。所以上一篇文章中后面想要实现的效果——隐藏标题栏和滚动条——思路都变得很清晰:可以找到对应的对象然后使用hide()
隐藏,或者查找对应对象的隐藏函数接口。
从图中也可以看到,各种Item-Based
控件都是从View
类派生而来。
2. 基本操作
由上一篇文章我们已经知道,QTableView
显示的行列数、数据、对齐方式、颜色等,都是由Model
类决定的,所以它的基本操作也就剩下这些了:
- 表格大小控制,继承了
QWidget
所以有QWidget::resize()
函数; - 表格行列尺寸、可见性控制:
columnWidth()、setColumnWidth()、setColumnHidden()、isColumnHidden()
等;自适应行/列尺寸:resizeColumnsToContents()
; - 表格标题栏操作:
horizontalHeader()
获取标题栏; - 表格滚动条操作:
horizontalScrollBar()、setHorizontalScrollBar()、setHorizontalScrollBarPolicy()
; - 网格显示与文字省略:
setShowGrid()、setWordWrap()
;
以上内容,针对
column
的函数也适用于row
,针对horizontal
的函数也适用于vertical
。
针对这些内容,个人比较推荐知道就好,这边的介绍也十分简略。就像ASCII码表,知道它能用转义字符表示回车、振铃之类的就好,等到确实需要使用的时候再去查码表即可。
3. 进阶
这部分内容基本上是上一篇讲自定义Model
类文章的后续。稍微回顾一下上一篇都干了啥:
- 搞一堆稀碎的数据结构(单变量+数组+结构体);
- 把数据一个个对应成
4*5
的表格;- 显示表格内容,并实现编辑功能;
- 稍微改了改表格的外观,使之看上去不那么low(虽然依旧…)。
说实话这根本不行,太过死板,不是这样打的~
本小节从QAbstractTableModel
派生自己的Model
类,实现可修改的动态表格。
2.1 数据结构设计
本例使用QTableView
来展现一个可编辑的书籍列表,设计如下数据结构:
//一本书的属性
struct Book
{
QString name; // 书名
QString publisher; // 出版社
QString type; // 类别
double price; // 价格
};
//属性对应的表头(列表头)
const QStringList titles = {"书名", "出版社", "类别", "价格"};
此处作为示例,书本属性比较简单。
打算显示一个行数可变的、可编辑表格。如果也希望列数可变,则自行再设计数据结构即可,涉及到的派生类代码难度区别不大。
2.2 核心代码
首先,根据上一篇文章的套路,由QAbstractTableModel
派生出我们自己的MyTableModel
类,实现一下基本的显示和修改功能。其中表头不再被隐藏,水平表头采用数据中的titles
变量内容,竖直表头采用默认编号,这主要通过重写headerData()
实现。以下为主要代码:
virtual int rowCount(const QModelIndex &/*parent*/ = QModelIndex()) const override
{
// 书本数量,bookList为私有变量,存储书本信息的数组
return bookList.size();
}
virtual int columnCount(const QModelIndex &/*parent*/ = QModelIndex()) const override
{
// 书本属性值数量
return titles.size();
}
// 核心函数,View类从Model中取数据,传入的参数在之前的文章中有介绍
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
{
if(!index.isValid() || index.column()>=columnCount() || index.row()>=rowCount()) return QVariant();
switch(role)
{
// 显示数据用
case Qt::DisplayRole:
switch(index.column())
{
case 0: // 书名
return bookList[index.row()].name;
break;
case 1: // 出版社
return bookList[index.row()].publisher;
break;
case 2: // 类型
return bookList[index.row()].type;
break;
case 3: // 价格
return bookList[index.row()].price;
break;
default:
return QVariant();
break;
}
break;
// 对齐处理
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
break;
// 其余不处理
default:
return QVariant();
break;
}
}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const
{
// 水平表头显示信息
switch(role)
{
case Qt::DisplayRole:
if(orientation==Qt::Horizontal && section>=0 && section<=columnCount())
return titles.at(section);
break;
default:
break;
}
return QAbstractItemModel::headerData(section, orientation, role);
}
// 编辑相关函数
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
{
if(role != Qt::EditRole || !index.isValid() || index.row()>=rowCount() || index.column()>=columnCount()) return false;
bool ok = false;
switch(index.column())
{
case 0: // 书名
bookList[index.row()].name = value.toString();
break;
case 1: // 出版社
bookList[index.row()].publisher = value.toString();
break;
case 2: // 类型
bookList[index.row()].type = value.toString();
break;
case 3: // 价格
// 简单判断是否为double类型
value.toDouble(&ok);
if(ok) bookList[index.row()].price = value.toDouble(&ok);
else return false;
break;
default:
return false;
break;
}
emit dataChanged(index, index);
return true;
}
private:
// 存储书本信息的数组
QVector<Book> bookList;
然后实现对数据行的修改功能。依据QAbstractTableModel
的Subclassing
部分,要添加行,需要重写insertRows()
接口,并在修改数据结构之前调用beginInsertRows()
函数,在修改完数据之后立刻调用endInsertRows()
函数;要删除行则需要实现对应的remove
函数。下图为官方文档中的描述,作为参考:
看起来也不难,实现一下:
private:
// 行修改函数:添加多行和删除多行
virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override
{
// 起始行row超限时,修正到两端插入
if(row > rowCount()) row = rowCount();
if(row < 0) row = 0;
// 需要将修改部分的代码使用begin和end函数包起来
beginInsertRows(parent, row, row+count-1);
// 添加数据
for(int i = 0; i < count; ++i) bookList.insert(bookList.begin()+row+i, Book());
endInsertRows();
emit dataChanged(createIndex(row, 0), createIndex(row+count-1, columnCount()-1));
return true;
}
virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex())
{
if(row < 0 || row >= rowCount() || row + count > rowCount()) return false;
// 需要将修改部分的代码使用begin和end函数包起来
beginRemoveRows(parent, row, row+count-1);
// 删除数据
for(int i = 0; i < count; ++i)
{
bookList.remove(row);
}
endRemoveRows();
return true;
}
public:
// 2个简单公有接口
// 最后添加一行
void appendRow()
{
// insertRow为内联函数,重写insertRows后即有,removeRow也是内联函数
insertRow(rowCount());
}
// 最后删除一行
void popBack()
{
removeRow(rowCount()-1);
}
最后,在main()
函数中添加如下代码,调用我们的MyTableModel
类。
// view和model联动
QTableView *tbl = new QTableView;
MyBookTableModel model;
tbl->setModel(&model);
// 隐藏滚动条
tbl->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
tbl->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// 准备主界面
QWidget w;
// 2个按钮,用来改变行数,采用垂直布局
QVBoxLayout *v = new QVBoxLayout;
QPushButton *btn1 = new QPushButton("添加一行");
QPushButton *btn2 = new QPushButton("删除最末行");
qApp->connect(btn1, &QPushButton::clicked, [&](){
model.appendRow();
});
qApp->connect(btn2, &QPushButton::clicked, [&](){
model.popBack();
});
v->addWidget(btn1);
v->addWidget(btn2);
// 主界面的布局采用水平布局,左边是表格,右边是刚才的2个按键
QHBoxLayout *h = new QHBoxLayout;
h->addWidget(tbl);
h->addLayout(v);
// 设置主界面的布局
w.setLayout(h);
w.show();
// 当内容改变时自适应列宽
qApp->connect(&model, &MyBookTableModel::dataChanged,[&](){
tbl->resizeColumnsToContents();
int width = 0;
for(int i = 0; i < model.columnCount(); ++i)
{
width += tbl->columnWidth(i);
}
width += tbl->verticalHeader()->width();
// 设置表格最小宽度
tbl->setMinimumWidth(width);
// 设置主窗口大小以刷新界面布局
w.resize(w.minimumWidth(), 300);
});
// 最开始添加2行
model.appendRow();
model.appendRow();
运行起来看一下效果:
显示基本没啥问题~修改内容试一试发现也没啥问题,内容修改以后界面会自适应大小,添加和删除行也都OK。
4. 小结
QTableView
是封装较为完备的类,除了设置尺寸、可见性、网格、文字省略等表面工作,有关内容的部分都可以在Model
类中完成;优雅地使用QTableView
就是View
类轻量化,专注于实现Model
即可;- 重写
headerData()
可以在Model
类中处理水平或者垂直表头,处理办法类似data()
函数; - 通过重写实现
insertRows()
和removeRows()
函数后,可以对表格的行数进行修改,不过需要将修改数据的代码包裹在对应的begin和end
函数中; - 完整的工程代码在此处下载。
如有错误欢迎指正,共同进步~
今天你学废了吗?