Qt Model/View 学习(5.5) - 使用QTableView(优雅地)实现翻页效果(附源码)


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. 思路

朋友的提问是:用QAbstractTableModelQTableView翻页显示,而不是用滚动条的形式。

上一篇文章我们了解到,QTableView继承了QAbstractScrollArea,天然地就支持滚动条,但这里不让用滚动条。

但是,我们其实可以假装没用滚动条:界面上看起来是翻页效果,但逻辑还是用的滚动条在做。

这个思路的关键步骤就是,让滚动条一次的步子迈大一些刚好跳过一页的内容,就像下图这样,红框是当前显示的内容,只要计算出每一步的步长应该就OK了。
在这里插入图片描述
大概想一想,需要做的事情有:

  1. QAbstractTableModel派生自己的Model类,填好数据结构以及每页显示的行数和对应接口
  2. QTableView派生自己的View类,重写滚动条函数wheelEvent(),使之刚好跳过整整一页;其中需要使用model()接口,获取到Model类指针,转换成自己的Model类后获取每页行数,从而计算每一页要滚动的步长;
  3. View类中添加切换页面的接口,将该接口与软件界面的某些按键关联起来;

感觉也不难嘛~但是随即想到了一个问题:对于QAbstractScrollArea来说,如果最后一页的内容并不够填满一页,将滚动条滑到底将显示的内容不太对劲,如下图:

在这里插入图片描述
那好像也不难:只要在Model类中,将行数rowCount()设置为每一页行数的倍数即可。

思路打通以后,要着手实现的时候,我陷入了沉思:不够优雅
在这里插入图片描述
上篇文章才说的QTableView轻量化,这就直接打脸了,要派生QTableView,而且ModelView搅来搅去,岂止是不够优雅,简直太过寒碜!
在这里插入图片描述

那就立个小目标:优雅地实现翻页效果~

2. 思路2.0

既然打算优雅,那就静下心来好好地推理一下:

  1. 要实现翻页效果,但不能派生自己的View。那就意味着别想使用滚动条来跳页了。
  2. 不使用滚动条来跳页,那就意味着View的全部行数只能是一页想要显示的行数了,也就意味着rowCount()返回的值也是一页显示的行数了;
  3. rowCount()返回值固定,而数据长度不固定,那就需要设计当前页显示数据的逻辑了,意味着data()函数需要按照当前页码来选择对应数据来显示
  4. ???好像已经通了!!!

核心思路就是:总行数固定不变,根据当前页码显示对应的数据~

这个思路有得搞,可以优雅起来的样子,我们撸一下代码~

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 完善细节

基本功能虽然没啥问题了,但至少还应该注意这两点:

  1. 应该显示一下最大页数;
  2. 添加/删除数据时,希望能定位到变动的那一页;

首先第一个问题,给出一个接口获取最大页数,给出一个信号,当最大页数变化时发出信号,并在添加和删除条目时,合理触发信号。

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();
    }

大概一想,跳页函数调用的逻辑有:

  1. 添加新数据时,跳转到新数据所在的那一页
  2. 删除数据时,跳转到被删除数据的后一条数据所在页,如果不存在后一条,则跳转到前一条数据所在页

在添加和删除数据的函数中修改:

    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. 小结

  1. 实现翻页的思路:View行数不变,页数改变取对应偏移地址的数据来显示;这样我们还能继续优雅地使用QTableView
  2. 一开始的思路没有经过实践,有可能根本跑不通……如果有朋友有想法可以共同交流一下~
  3. 数据的插入和删除,只需要修改源数据然后刷新显示,而不用重写insertRows()等函数,这些是用于View行数改变的场合;
  4. 最大页数改变的条件为:新添加一条数据后,size()%pageRowCount==1;删除一条数据后,size()%pageRowCount==0
  5. 上一篇文章的代码在这里,本篇修改之后的代码在这里,感兴趣的朋友也可以自行修改试一试~

如有错误欢迎指正,共同进步~


今天你学废了吗?

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值