0. 前言
在上一篇文章中,我们讲了QTableView
的优雅使用教程,强调了要优雅地使用QTableView
,就要好好设计自己的Model
类。
然后,上篇文章还发生了神奇的事情:系列文章迎来了第一条评论!!!
锣鼓喧天,鞭炮齐鸣~这不得触发一下支线任务,尝试解决一下这位朋友的提问?( 反正下一篇该写的Delegate还没看懂 )
所以本文内容旨在给出一个思路和关键代码,以实现翻页的QTableView
效果。中间会提到思路形成的过程等,仅供参考~如果朋友们有更精妙的思路,欢迎评论/私信提出,共同探讨!
系列文章回顾:
Qt Model/View 学习(1) - 是什么和为什么?
Qt Model/View 学习(2) - QModelIndex索引模型数据
Qt Model/View 学习(3) - 索引来一堆东西,究竟取谁(ItemDataRole)?
Qt Model/View 学习(4) - 实现自己的QAbstractTableModel类(支持显示与修改)
Qt Model/View 学习(5) - QTableView(优雅)使用教程(附源码)
1. 思路
朋友的提问是:用QAbstractTableModel
和QTableView
做翻页显示,而不是用滚动条的形式。
从上一篇文章我们了解到,QTableView
继承了QAbstractScrollArea
,天然地就支持滚动条,但这里不让用滚动条。
但是,我们其实可以假装没用滚动条:界面上看起来是翻页效果,但逻辑还是用的滚动条在做。
这个思路的关键步骤就是,让滚动条一次的步子迈大一些,刚好跳过一页的内容,就像下图这样,红框是当前显示的内容,只要计算出每一步的步长应该就OK了。
大概想一想,需要做的事情有:
- 从
QAbstractTableModel
派生自己的Model
类,填好数据结构以及每页显示的行数和对应接口; - 从
QTableView
派生自己的View
类,重写滚动条函数wheelEvent()
,使之刚好跳过整整一页;其中需要使用model()
接口,获取到Model
类指针,转换成自己的Model
类后获取每页行数,从而计算每一页要滚动的步长; - 在
View
类中添加切换页面的接口,将该接口与软件界面的某些按键关联起来;
感觉也不难嘛~但是随即想到了一个问题:对于QAbstractScrollArea
来说,如果最后一页的内容并不够填满一页,将滚动条滑到底将显示的内容不太对劲,如下图:
那好像也不难:只要在Model
类中,将行数rowCount()
设置为每一页行数的倍数即可。
思路打通以后,要着手实现的时候,我陷入了沉思:不够优雅。
上篇文章才说的QTableView
轻量化,这就直接打脸了,要派生QTableView
,而且Model
和View
搅来搅去,岂止是不够优雅,简直太过寒碜!
那就立个小目标:优雅地实现翻页效果~
2. 思路2.0
既然打算优雅,那就静下心来好好地推理一下:
- 要实现翻页效果,但不能派生自己的
View
类。那就意味着别想使用滚动条来跳页了。 - 不使用滚动条来跳页,那就意味着View的全部行数只能是一页想要显示的行数了,也就意味着
rowCount()
返回的值也是一页显示的行数了; rowCount()
返回值固定,而数据长度不固定,那就需要设计当前页显示数据的逻辑了,意味着data()
函数需要按照当前页码来选择对应数据来显示;- ???好像已经通了!!!
核心思路就是:总行数固定不变,根据当前页码显示对应的数据~
这个思路有得搞,可以优雅起来的样子,我们撸一下代码~
3. 代码实现
我们从上一篇文章实现效果的基础上进行修改(上篇文章的源码在此处),一步步修改MyTableModel
类,实现这个翻页效果。
3.1 修改行数处理
首先,添加翻页所需要的变量,修改每页要显示的行数:
public:
virtual int rowCount(const QModelIndex &/*parent*/ = QModelIndex()) const override
{
// 改为每页行数
return pageRowCount;
}
virtual int columnCount(const QModelIndex &/*parent*/ = QModelIndex()) const override
{
// 书本属性值数量
return titles.size();
}
private:
// 每页显示的数量
int pageRowCount = 5;
// 当前是第几页
int currentPage = 0;
3.2 修改数据获取
QTableView
通过Model
类的data()
函数获取数据,修改该函数,返回跳过前面页面所有数据的偏移数据:
// 核心函数,View类从Model中取数据,传入的参数在之前的文章中有介绍
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
{
if(!index.isValid() || index.column()>=columnCount() || index.row()>=rowCount()) return QVariant();
// 根据当前页面,计算偏移
int offset = currentPage*pageRowCount;
// 再次判断
if(index.row() + offset >= bookList.size()) return QVariant();
switch(role)
{
// 显示数据用
case Qt::DisplayRole:
switch(index.column())
{
case 0:
return bookList[index.row() + offset].name;
break;
case 1:
return bookList[index.row() + offset].publisher;
break;
case 2:
return bookList[index.row() + offset].type;
break;
case 3:
return bookList[index.row() + offset].price;
break;
default:
return QVariant();
break;
}
break;
// 对齐处理
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
break;
// 其余不处理
default:
return QVariant();
break;
}
}
对于修改数据函数setData()
,我们采用同样的偏移量offset = currentPage*pageRowCount
来定位数据,此处不再贴源码。
3.3 翻页处理
在Model
类中添加翻页处理,下边界就是0,稍微注意处理好上边界就行:只要到当前页为止,还没能显示完所有数据,就有下一页。另外,再添加一个页数改变的信号,以及用于刷新界面显示用的函数。
public:
// 添加翻页接口
// 上一页
void pageUp()
{
if(currentPage > 0)
{
--currentPage;
emit pageChanged(currentPage);
updateData();
}
}
// 下一页
void pageDown()
{
if( (currentPage+1)*pageRowCount < bookList.size() )
{
++currentPage;
emit pageChanged(currentPage);
updateData();
}
}
// 获取当前页
int getCurrentPage() const
{
return currentPage;
}
// 留一个便利的刷新接口
void updateData()
{
emit dataChanged(createIndex(0, 0), createIndex(rowCount(), columnCount()));
}
signals:
// 页数改变信号
void pageChanged(int curPage);
3.4 main()函数
到此,Model
类的显示和修改就基本完成了,在main()
函数中添加以下代码,显示我们的表格、翻页按钮和当前页数。
其中的延时更新代码,建议自己试一试不延时会怎样~
// 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("下一页");
QLabel *lbl = new QLabel;
qApp->connect(btn1, &QPushButton::clicked, [&](){
model.pageUp();
});
qApp->connect(btn2, &QPushButton::clicked, [&](){
model.pageDown();
});
// 显示第几页
qApp->connect(&model, &MyBookTableModel::pageChanged, [=](int now){
lbl->setText(QString("第%1页").arg(now+1));
});
v->addWidget(btn1, 0, Qt::AlignCenter);
v->addWidget(lbl, 0, Qt::AlignCenter);
v->addWidget(btn2, 0, Qt::AlignCenter);
// 主界面的布局采用水平布局,左边是表格,右边是刚才的2个按键
QHBoxLayout *h = new QHBoxLayout;
h->addWidget(tbl);
h->addLayout(v);
// 设置主界面的布局
w.setLayout(h);
w.show();
// 当内容改变时自适应列宽
// 延时更新,可以试一下,不延时更新会有怎样的问题?
QTimer timer;
qApp->connect(&timer, &QTimer::timeout, [&](){
w.resize(w.minimumWidth(), w.minimumHeight());
});
timer.setInterval(20);
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();
int height = 0;
for(int i = 0; i < model.rowCount(); ++i)
{
height += tbl->rowHeight(i);
}
height += tbl->horizontalHeader()->height();
// 设置表格最小尺寸
tbl->setMinimumSize(width, height);
// 设置主窗口大小以刷新界面布局
timer.start();
});
// 最开始触发界面刷新
model.updateData();
emit model.pageChanged(0);
先偷个懒,在Model
类的构造函数中,将数据初始数量修改为14个,Ctrl+R
运行起来看看效果~
翻页显示都没问题,修改数据试一下也都OK:
3.5 数据插入/删除
上一篇文章讲过,通过重写Model
类的insertRows()
和removeRows()
函数来实现行数改变,但这回不是这样了。因为View的行数恒定,所以只要给出新接口,直接对源数据进行修改,然后刷新一下显示即可~
// 给出简单的数据添加和删除接口
public:
void insertData(int startIndex, const Book& book=Book())
{
if(startIndex<0 || startIndex > bookList.size()) return;
bookList.insert(bookList.begin()+startIndex, book);
// 改变了数据后刷新界面
updateData();
}
void removeData(int startIndex, int num=1)
{
if(startIndex<0 || startIndex >= bookList.size()) return;
for(int i=0; i<num && i+startIndex<bookList.size(); ++i)
bookList.remove(startIndex);
// 改变了数据后刷新界面
updateData();
}
void appendData(const Book& book=Book())
{
insertData(bookList.size(), book);
}
void removeLast()
{
removeData(bookList.size()-1);
}
然后在main()
函数中,再添加2个按钮,用来添加最后一行和删除最后一行,代码如下:
// 添加数据和删除数据按钮
QPushButton *btn3 = new QPushButton("末尾添加一行");
QPushButton *btn4 = new QPushButton("末尾删除一行");
qApp->connect(btn3, &QPushButton::clicked, [&](){
model.appendData();
});
qApp->connect(btn4, &QPushButton::clicked, [&](){
model.removeLast();
});
v->addWidget(btn3, 0, Qt::AlignCenter);
v->addWidget(btn4, 0, Qt::AlignCenter);
在MyTableModel
的构造函数中,将初始数据数量改为0,Ctrl+R
看看效果~发现添加行和删除行也没啥问题。
3.6 完善细节
基本功能虽然没啥问题了,但至少还应该注意这两点:
- 应该显示一下最大页数;
- 添加/删除数据时,希望能定位到变动的那一页;
首先第一个问题,给出一个接口获取最大页数,给出一个信号,当最大页数变化时发出信号,并在添加和删除条目时,合理触发信号。
public:
int getMaxPage() const
{
// 0页意味着没有条目,后面需要用括号括起来
return bookList.size()/pageRowCount + ((bookList.size()%pageRowCount)!=0);
}
signals:
// 触发条件:添加数据时size()%pageRowCount==1,删除数据时size()%pageRowCount==0
void maxPageChanged(int maxPage);
并在main()
函数中修改页数显示的内容,此处还顺手修复了一下没有数据也显示第1页的问题:
// 显示第几页和最大页数
qApp->connect(&model, &MyBookTableModel::pageChanged, [&](int now){
lbl->setText(QString("第%1/%2页").arg(model.getMaxPage()==0?0:now+1).arg(model.getMaxPage()));
});
qApp->connect(&model, &MyBookTableModel::maxPageChanged, [&](int max){
lbl->setText(QString("第%1/%2页").arg(max==0?0:model.getCurrentPage()+1).arg(max));
});
运行一下发现另一个问题:当前页的最后一条数据被删除时,会出问题:
这时候我们至少希望能自动跳到上一页去。正好这个功能和要完善的第2点类似,我们先写一个跳页的函数:
// 跳页函数
void changeCurrentPage(int page)
{
if(page<0 || page>getMaxPage() || currentPage==page) return;
currentPage=page;
emit pageChanged(page);
updateData();
}
大概一想,跳页函数调用的逻辑有:
- 添加新数据时,跳转到新数据所在的那一页;
- 删除数据时,跳转到被删除数据的后一条数据所在页,如果不存在后一条,则跳转到前一条数据所在页;
在添加和删除数据的函数中修改:
void insertData(int startIndex, const Book& book=Book())
{
if(startIndex<0 || startIndex > bookList.size()) return;
bookList.insert(bookList.begin()+startIndex, book);
if(bookList.size()%pageRowCount==1) emit maxPageChanged(getMaxPage());
changeCurrentPage(startIndex/pageRowCount);
// 改变了数据后刷新界面
updateData();
}
void removeData(int startIndex, int num=1)
{
if(startIndex<0 || startIndex >= bookList.size()) return;
for(int i=0; i<num && i+startIndex<bookList.size(); ++i)
{
bookList.remove(startIndex);
if(bookList.size()%pageRowCount==0) emit maxPageChanged(getMaxPage());
// 存在后一条记录
if(bookList.size()>startIndex) changeCurrentPage(startIndex/pageRowCount);
else changeCurrentPage((startIndex-1)/pageRowCount);
}
// 改变了数据后刷新界面
updateData();
}
运行看一下~似乎没啥问题了:添加数据或者删除数据都会跳转到对应页:
4. 小结
- 实现翻页的思路:
View
行数不变,页数改变时取对应偏移地址的数据来显示;这样我们还能继续优雅地使用QTableView
; - 一开始的思路没有经过实践,有可能根本跑不通……如果有朋友有想法可以共同交流一下~
- 数据的插入和删除,只需要修改源数据然后刷新显示,而不用重写
insertRows()
等函数,这些是用于View
行数改变的场合; - 最大页数改变的条件为:新添加一条数据后,
size()%pageRowCount==1
;删除一条数据后,size()%pageRowCount==0
。 - 上一篇文章的代码在这里,本篇修改之后的代码在这里,感兴趣的朋友也可以自行修改试一试~
如有错误欢迎指正,共同进步~
今天你学废了吗?