Qt Model/View 学习(5) - QTableView(优雅)使用教程(附源码)


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类决定的,所以它的基本操作也就剩下这些了:

  1. 表格大小控制,继承了QWidget所以有QWidget::resize()函数;
  2. 表格行列尺寸、可见性控制:columnWidth()、setColumnWidth()、setColumnHidden()、isColumnHidden()等;自适应行/列尺寸:resizeColumnsToContents()
  3. 表格标题栏操作:horizontalHeader()获取标题栏;
  4. 表格滚动条操作:horizontalScrollBar()、setHorizontalScrollBar()、setHorizontalScrollBarPolicy()
  5. 网格显示与文字省略:setShowGrid()、setWordWrap()

以上内容,针对column的函数也适用于row,针对horizontal的函数也适用于vertical

针对这些内容,个人比较推荐知道就好,这边的介绍也十分简略。就像ASCII码表,知道它能用转义字符表示回车、振铃之类的就好,等到确实需要使用的时候再去查码表即可。


3. 进阶

这部分内容基本上是上一篇讲自定义Model类文章的后续。稍微回顾一下上一篇都干了啥:

  1. 搞一堆稀碎的数据结构(单变量+数组+结构体);
  2. 把数据一个个对应成4*5的表格;
  3. 显示表格内容,并实现编辑功能;
  4. 稍微改了改表格的外观,使之看上去不那么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;

然后实现对数据行的修改功能。依据QAbstractTableModelSubclassing部分,要添加行,需要重写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. 小结

  1. QTableView是封装较为完备的类,除了设置尺寸、可见性、网格、文字省略表面工作,有关内容的部分都可以在Model中完成;优雅地使用QTableView就是View类轻量化,专注于实现Model即可;
  2. 重写headerData()可以在Model类中处理水平或者垂直表头,处理办法类似data()函数;
  3. 通过重写实现insertRows()removeRows()函数后,可以对表格的行数进行修改,不过需要将修改数据的代码包裹在对应的begin和end函数中;
  4. 完整的工程代码在此处下载

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


今天你学废了吗?

  • 12
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值