QML高效率Table的实现

本文探讨了在QML中遇到的TableView列过多导致效率低下的问题,并介绍了Qt 5.12新引入的更高效的TableView。新TableView以单元格为基础进行绘制,显著提升了性能,但缺少表头和行概念,需要自定义实现。文章提供了自定义解决方案及部分代码示例。

TableView超多列的解决办法

问题描述

在QML中我们经常商用TableView来进行数据的展示。大部分情况下使用Qt Quick Controls 1自带的TableView是问题不大的。但在某些情况下则会有力不从心的感觉,比如表格的列可能有一百甚至数百列的时候就会出现刷新效率低下的问题。

查看TableView的源码你会发现这是TableView的实现是以行为基本单位,在行数很多时只会实现真正显示的那十几行的委托。所以在行数过多时问题也不大。

但在Qt Quick Controls 1中的TableView只针对行进行了优化,它实现时没有考虑列数过多的情况。当表格的列有300列时,哪怕只刷新表格显示的20行,也会有将近6000个对象进行绘制。

解决办法

对于这个问题,你可以将Qt Quick Controls 1的TableView源码分离出来,自己针对这一部分进行优化。但这个方法比较费力不讨好,出现新的bug的可能性更大。

索性Qt官方也知道自己的这个问题了,所以在后来增加了一个新的TableView。下面针对这个新的TableView说一下。

前提条件

在Qt 5.12的版本中官方增加了这个他们宣传更加高效快速的TableView。所以如果你的无法升级到Qt 5.12以上的版本,这篇文章对你意义不大可以右上角了。

新TableView的引用

众所周知Qt Quick Controls 1有很多问题,Qt官方处于种种原因又重复造了一次轮子这就是Qt Quick Controls 2。而这里介绍到的TableView也是Qt官方在5.12版本又造的一次轮子。所以在QML项目中如果不注意应用很容易混淆这两个控件。

  • 使用之前的TableView一般是引用以下插件
import QtQuick.Controls 1.4

在Qt creator中是这些属性
在这里插入图片描述

  • 而新版本的TableVew是来自以下插件
import QtQuick 2.12

在Qt creator中是这些属性
在这里插入图片描述

新TableView的效果

Qt官方给出了两个TableView的效果,其实我更愿意称之为Excel,其用法和我们想要的TableView效果差距还是很大的。
在这里插入图片描述
看到这个图大伙可能知道我为什么这么说了。因为这个Table没有表头没有列名……

新TableView的问题

新TableView是以单元格为基本单位进行绘制,也就是说它只会绘制你表格显示在界面中的数据。即时你有3000列x1000行,它也只会绘制你界面实际显示的6x20行。绘制对象少了不止一个量级。

所以在效率上确实不需要担心。本人实测300列插入10000行的时间也不到1ms。

  • 但有利有弊,以单元格作为基本单位进行选择绘制的第一个问题就是新TableView没有行的概念!
    在这里插入图片描述
    这句话的意思不是没有行数这个属性了,在新TableView中还是有rows这个参数的,我说没有行是指不存在类型之前TableView的rowDelegate这种用法。

  • 第二个问题就是新TableView不自带表头
    所以得自己去处理列名、表头绘制……

  • 第三个问题就是无法再通过role属性直接访问表格的值

自定义TableView

如果你看到这里还没有放弃看下去的想法,那看来也是被QML的这个问题逼得够呛。如果你没有更好的解决方案暂且看看我的例子,希望对你有所帮助。

代码我也传一份到CSDN,为了照顾部分没分的同学,我这里贴一些。写的时候部分地方用到了公司的代码所有进行了部分调整,颜色搭配需要下下来后再次调整。

  1. 自定义表头
    新TableView没有表头只有我们自己来手动实现一个,这部分算最简单的。几十行代码也够用了。
    一下是TableHead的代码

TableHeader.qml

import QtQuick 2.12
import QtQuick.Controls 2.12

Item{
    id: header_horizontal

    //暂存鼠标拖动的位置
    property int posXTemp: 0
    MouseArea{
        anchors.fill: parent
        onPressed: header_horizontal.posXTemp=mouseX;
        onPositionChanged: {
            if(table_view.contentX+(header_horizontal.posXTemp-mouseX)>0){
                table_view.contentX+=(header_horizontal.posXTemp-mouseX);
            }else{
                table_view.contentX=0;
            }
            header_horizontal.posXTemp=mouseX;
        }
    }
    Row {
        id: header_horizontal_row
        anchors.fill: parent
        leftPadding: -table_view.contentX
        clip: true

        Repeater {
            model: table_view.columns > 0 ? table_view.columns : 0

            Rectangle {
                id: header_horizontal_item
                width: table_view.columnWidthProvider(index)+table_view.columnSpacing
                height: parent.height
                color: "red"//自己配色


                border.width: 1
                border.color: "#1a2b3c"


                Label {
                    width:parent.width
                    height:control.horHeaderHeight
                    horizontalAlignment: Text.AlignHCenter
                    //anchors.fill: parent
                    text: _sourceModel.headerData(index,Qt.Horizontal)
                }

                Rectangle {
                    width: parent.width
                    height: filterMode*25
                    anchors.bottom: parent.bottom
                    color: "blue"
                    visible:height !== 0
                    TextInput{
                        anchors.fill: parent
                        onTextChanged:{
                          //  console.log(text,index)
                            //设置过滤条件
                            _displayModel.setCondition(index,text)

                            table_view.forceLayout()
                        }
                    }
                    Behavior on height {
                        PropertyAnimation{ duration : 200 }
                    }
                }





                Rectangle{
                    width: 1
                    height: parent.height
                    anchors.right: parent.right
                    color: "black"
                    opacity: 0.5
                }
                MouseArea{
                    width: 3
                    height: parent.height
                    anchors.right: parent.right
                    cursorShape: Qt.SplitHCursor
                    onPressed: header_horizontal.posXTemp=mouseX;
                    onPositionChanged: {
                        if((header_horizontal_item.width-(header_horizontal.posXTemp-mouseX))>10){
                            header_horizontal_item.width-=(header_horizontal.posXTemp-mouseX);
                        }else{
                            header_horizontal_item.width=10;
                        }
                        header_horizontal.posXTemp=mouseX;

                        //改变某列的宽度
                        control._columnWidthArr[index]=(header_horizontal_item.width-table_view.columnSpacing);
                        //刷新布局,这样宽度才会改变
                        table_view.forceLayout();
                    }
                }
            }
        }
    }
}

TableScrollBar.qml


import QtQuick 2.12
import QtQuick.Controls 2.5

ScrollBar {
    id:scroRoot
  //  policy: ScrollBar.AlwaysOn
    contentItem: Rectangle{
        visible: (scroRoot.size<1.0)
        color: scrollBarColor
    }
}

TableCell.qml

import QtQuick 2.12

import QtQuick.Controls 1.4

Rectangle{
    color: currentIndex === model.row ?
               "green" :"#1a2b3c"

    border.width :0
    Label{
        anchors.fill: parent
        verticalAlignment: Text.AlignVCenter
        horizontalAlignment: Text.AlignHCenter
        //elide: Text.ElideRight
        text:model.value
    }

    Rectangle {
        height: 2
        width: parent.width
        anchors.bottom: parent.bottom
        color: "#00000000"
        border.color: "white"
    }
}

EfficientTable.qml

import QtQuick 2.12

//import QtQuick.Controls.Styles 1.4
import QtQuick.Controls 2.12
import EfficientModel 0.1
import SortFilterModel 0.1

//自定义QtQuick 2中的TableView
Item {
    id: control


    //行表头-竖向的
    property int rowHeight: 30

    //列表头-横向的
    property int horHeaderHeight: 30

    //滚动条
    property color scrollBarColor: "yellow"
    property int scrollBarWidth: 11
    //行数
    property alias  rows: table_view.rows
    //当前选中行
    property int currentIndex: -1

    //是否使用过滤模式
    property bool filterMode:true

    property alias  table: table_view

    //列宽
    property variant _columnWidthArr: []


    EfficientModel{
        id:_sourceModel
    }

    SortFilterModel {
        id:_displayModel
    }

    Component.onCompleted: {
        _displayModel.setModel(_sourceModel)
    }

    //表格内容(不包含表头)
    TableView{
        id: table_view
        anchors{
            fill: parent
            topMargin: _tableHeader.height
        }

        clip: true
        boundsBehavior: Flickable.StopAtBounds

        //rowSpacing: 1

        //视图的高度
        //contentHeight: rowHeightProvider(0) * rows + rowHeightProvider(rows-1)
        //视图的宽度
        //contentWidth:
        //content内容区域边距,但是不影响滚动条的位置
        //leftMargin:
        //topMargin:
        //此属性可以包含一个函数,该函数返回模型中每行的行高
        rowHeightProvider: function (row) {
            return control.rowHeight;
        }
        //此属性可以保存一个函数,该函数返回模型中每个列的列宽
        columnWidthProvider: function (column) {
            return control._columnWidthArr[column];
        }
        ScrollBar.vertical: TableScrollBar {
            id: scroll_vertical
            anchors.right: parent.right
            implicitWidth: scrollBarWidth
        }

        ScrollBar.horizontal: TableScrollBar {
            id: scroll_horizontal
            anchors.bottom: parent.bottom
            implicitHeight:scrollBarWidth
        }
        //model是在C++中定义的
        model:_displayModel

        delegate: TableCell{}
        MouseArea{
            anchors.fill: parent
            onClicked: {
                control.currentIndex = parseInt(mouse.y/control.rowHeight)
            }
        }
    }


    //横项表头
    TableHeader{
        id:_tableHeader
        anchors{
            left: parent.left
            right: parent.right
        }
        height: control.horHeaderHeight + filterMode*25
        Behavior on height {
            PropertyAnimation{ duration : 200 }
        }
        z: 2
    }


    //添加新列
    function insertColumn(tableIndex,columnName,roleName,width){
        _columnWidthArr.splice(tableIndex,0,width)
        _sourceModel.insertColumn(tableIndex,columnName,roleName)
    }
    //添加数据
    function insertRow(row,data){
        _sourceModel.insertRow(row,data)
    }
    //添加数据
    function insertRows(row,datas){
        _sourceModel.insertDatas(row,datas)
    }


    //删除数据
    function removeRow(row,count){
        _sourceModel.removeRows(row,count)
    }
}

main.qml

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 1.4
Window {
    id:root
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")


    EfficientTable {
        id:tableRoot
        anchors.fill: parent;
        anchors.bottomMargin: 50
    }



    /*
    TabView{
        id:tabRoot

        Tab{
            title: "第一页"

        }

        Tab{
            title: "第二页"
            EfficientTable {
                anchors.fill: parent
            }
        }
        Tab{
            title: "第三页"
            EfficientTable {
                anchors.fill: parent
            }
        }
        Tab{
            title: "第四页"
            EfficientTable {
                anchors.fill: parent
            }
        }
    }*/



    Row {
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 0
        Button {
            text:"添加数据"
            onClicked: {
                if (dataTimer.running){
                    dataTimer.stop()
                }else{
                    dataTimer.start()
                }
            }
        }

        Button {
            text:"删除数据"
            onClicked: {

                tableRoot.removeRow(tableRoot.currentIndex,1)
            }
        }

        Button {
            text:"过滤数据"
            onClicked: {
                tableRoot.filterMode = !tableRoot.filterMode

            }
        }

        Button {
            text:"打印"
            onClicked: {
               console.log(tableRoot.rows)

            }
        }
    }


    Component.onCompleted: {

        for (var j=0; j <300;j++){
            tableRoot.insertColumn(j,"列"+String(j) , "C"+String(j) ,100)
        }

        dataTimer.start();
    }
    property var ss: 0
    Timer {
        id:dataTimer
        interval:1093
        repeat: true
        onTriggered: {



            console.time("test1")


            var data = {}
            for (var i=0; i < 300; i++){

                var roleName = "C"+String(i)
                data[roleName]= "----"+String(ss)

            }
            var datas = []
            for (var j=0; j <10000;j++){



                ss =  ss +1
                datas.push(data)

            }
            console.timeEnd("test1")
tableRoot.insertRows(tableRoot.rows,datas)
        //    tableRoot.insertRows(tableRoot.rows,datas)


          //  tableRoot.table.contentY = tableRoot.table.contentHeight +
           //         tableRoot.height
        }
    }
}

EfficientModel.h

#ifndef EFFICIENTMODEL_H
#define EFFICIENTMODEL_H

#include <QAbstractTableModel>
#include <QJsonObject>
#include <QJsonArray>
#include <QJSValue>

class EfficientModel : public QAbstractTableModel
{
    Q_OBJECT

public:
    explicit EfficientModel(QObject *parent = nullptr);

    // Header:
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

    // Basic functionality:
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;




    // Remove data:
    Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
    bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override;


    Q_INVOKABLE QHash<int,QByteArray> roleNames() const override;

   //插入新列
   Q_INVOKABLE bool insertColumn(int column, QString columnName,QString roleName);
   //插入新行
   Q_INVOKABLE bool insertRow(int row, QJSValue data,const QModelIndex &parent = QModelIndex());

   Q_INVOKABLE bool insertDatas(int row, QJSValue datas,const QModelIndex &parent = QModelIndex());
   //通过角色名获得对应的单元格的值
   //Q_INVOKABLE QVariant modelData(const int row, QString roleName) const ;
private:
    //表格的头部
    QStringList _header;
    //角色映射
    QMap<int,QString> _roleMap;

    //表格的原始数据
    QVector<QJSValue> _modelData;
};

#endif // EFFICIENTMODEL_H

EfficientModel.cpp

#include "EfficientModel.h"
#include <QDebug>
#include <QTime>
EfficientModel::EfficientModel(QObject *parent)
    : QAbstractTableModel(parent)
{


}

QVariant EfficientModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    // FIXME: Implement me!

    if(orientation==Qt::Horizontal&&role==Qt::DisplayRole){
        if (section < _header.size()){
            return _header.at(section);
        }
    }
    return QAbstractTableModel::headerData(section,orientation,role);
}

int EfficientModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;

    return _modelData.size();



    // FIXME: Implement me!
}

int EfficientModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;

    return _header.size();
    // FIXME: Implement me!
}

QVariant EfficientModel::data(const QModelIndex &index, int role) const
{

    Q_UNUSED(role)

    if (!index.isValid())
        return QVariant();

    int nRow = index.row();
    int nColumn = index.column();

    //找到映射
    if (!_roleMap.contains(nColumn)){
        return "";
    }

    if (nRow >= _modelData.size()){
        return "";
    }


    QString strRole = _roleMap.value(nColumn);

    return _modelData.at(nRow).property(strRole).toString();
  //  QJsonDocument jsonDoc = QJsonDocument::fromVariant(.toVariant());


}




bool EfficientModel::insertColumn(int column, QString columnName,QString roleName)
{

    beginInsertColumns(QModelIndex(), column, column);

    _header.insert(column,columnName);
    //记录角色名和列索引直接的关系
    _roleMap[column] = roleName;

    endInsertColumns();

    return true;
}

bool EfficientModel::removeRows(int row, int count, const QModelIndex &parent)
{
    beginRemoveRows(parent, row, row + count - 1);
    // FIXME: Implement me!
    _modelData.remove(row,count);
    endRemoveRows();
    return true;
}

bool EfficientModel::removeColumns(int column, int count, const QModelIndex &parent)
{
    beginRemoveColumns(parent, column, column + count - 1);
    // FIXME: Implement me!
    endRemoveColumns();
    return true;
}




bool EfficientModel::insertRow(int row,QJSValue data,const QModelIndex &parent)
{
    beginInsertRows(parent, row, row );

    _modelData.insert(row,data);
    endInsertRows();

    return true;
}

bool EfficientModel::insertDatas(int row, QJSValue datas,const QModelIndex &parent)
{

    int arrayCount = datas.property("length").toInt();
    beginInsertRows(parent, row, row+arrayCount -1 );
    for (int i =0; i < arrayCount; i++){
        _modelData.push_back(datas.property(i));
    }
    endInsertRows();
    return true;
}

QHash<int, QByteArray> EfficientModel::roleNames() const
{

    //value表示取值,edit表示编辑
    return QHash<int,QByteArray>{
        { Qt::DisplayRole,"value" },
        { Qt::EditRole,"edit" }
    };
}


SortFilterModel.h

#ifndef SORTFILTERMODEL_H
#define SORTFILTERMODEL_H

#include <QSortFilterProxyModel>
#include "EfficientModel.h"

class SortFilterModel : public QSortFilterProxyModel
{
    Q_OBJECT
public:
    explicit SortFilterModel(QObject *parent = 0);
    //设置过滤条件
    Q_INVOKABLE void setCondition(const int column, const QString& strCondition);


    Q_INVOKABLE QHash<int,QByteArray> roleNames() const override;

    Q_INVOKABLE void setModel(EfficientModel* pModel);
protected:
    //该函数为虚函数,继承下来可以重写,返回值决定了是否将内容显示到表格中
    virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;




signals:


private:
    //所有的过滤条件
    QMap<int,QString> _conditionMap;


};

#endif // SORTFILTERMODEL_H

SortFilterModel.cpp

#include "SortFilterModel.h"
#include <QDebug>
#include <QAbstractTableModel>
SortFilterModel::SortFilterModel(QObject *parent) :
    QSortFilterProxyModel(parent)
{
    //setDynamicSortFilter(false);

}

void SortFilterModel::setCondition(const int column, const QString& strCondition)
{
    //如果没有限制条件,则将过滤条件置为空
    if (strCondition.isEmpty()){
        _conditionMap.remove(column);
    }else{
        _conditionMap.insert(column,strCondition);
    }
 //setFilterFixedString("");

    invalidateFilter();
}

//该函数为虚函数,继承下来可以重写,返回值决定了是否将内容显示到表格中
bool SortFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{

    if (_conditionMap.size() == 0){
        return true;
    }
    //遍历所有的过滤条件,有一个不满足就不显示

    QMap<int, QString>::const_iterator iter = _conditionMap.constBegin();
    while (iter != _conditionMap.end() ) {
        QModelIndex index = sourceModel()->index(source_row, iter.key(), source_parent);

        QString strData = sourceModel()->data(index).toString();
        if (!strData.contains(iter.value())){
            return false;
        }
        iter++;
    }

    return true;

}


QHash<int, QByteArray> SortFilterModel::roleNames() const
{

    //value表示取值,edit表示编辑
    return QHash<int,QByteArray>{
        { Qt::DisplayRole,"value" },
        { Qt::EditRole,"edit" }
    };
}
void SortFilterModel::setModel(EfficientModel* pModel){
    setSourceModel(pModel);
}

代码大概就贴全了,这个是我之前自己测试时写的代码,在过滤那块我记得还有个bug。但TableView的大体思路就是这样,在实际项目中我也已经用上了,暂时没什么问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值