QTreeView 自定義model:大數據量顯示 -- 性能和內存的優化

簡述

前面簡單介紹過Qt的模型/視圖框架,提到了Qt預定義的幾個model類型:
QStringListModel:存儲簡單的字符串列表
QStandardItemModel:可以用於樹結構的存儲,提供了層次數據
QFileSystemModel:本地系統的文件和目錄信息
QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel:存取數據庫數據

一般情況下滿足需求了,不過有時候需要一些定製功能,或者是大量數據下對性能和開銷比較注重,覺得自帶的model無用功能太多效率比較低,這時候自定義model就比較適合了。
我使用自定義model 同時出於這兩方面需要,既爲了性能也爲了特殊功能。

參考資料

豆子《Qt學習之路2》中的幾篇關於自定義model的文章:

  • 自定義model之一: 自定義只讀模型
  • 自定義model之二: 自定義可編輯模型
  • 自定義model之三: 布爾表達式樹模型

效果

本篇文章寫的費了點功夫,爲了演示本章內容,花了幾個小時的時間整理代碼和示例。
因爲技術都應用在我的項目裏,實際所用的model實現了很多特殊功能,非常複雜,我要提煉出一個簡單可讀的demo。

如圖,分別演示了以常規的 QStandardItemModel 和使用自定義的model的效果。
示例中只使用了10W行的數據量級
運行程序你就會發現,常規model在初始化tree的過程就比自定義model慢很多,更可怕的是,它所佔用的內存開銷是自定義model的數倍甚至數十倍!數據量越大內存差距越明顯。
QTreeView 自定義model:大數據量顯示 -- 性能和內存的優化

這裏以10個一級節點班級,每個班級1W個學生,共10W條記錄的數據量測試:
QStandardItemModel 方法程序佔用總內存大概160多M,而自定義model 佔用的30多M。
而Qt一個簡單窗口程序本身有20多M內存。
可見自定義model顯示這10W條記錄基本沒使用多少內存,如果考慮百萬、千萬級別的數據,不使用自定義model或比較有效的優化方法,內存將很快耗盡。

構造演示數據

我演示的例子爲一級節點班級、二級節點學生信息。

其中學生信息原始數據只有姓名、三門課成績,需顯示的列多一些,包含:
班級/姓名、語文、數學、外語、總分、平均分、是否合格、是否評優

其中後面幾列是根據學生成績計算得出的:
所有課成績都>60則合格,所有課成績都>90則優秀。

定義數據類型:班級、學生

//學生信息
typedef struct _STUDENT{
    QString name;   //姓名
    int score1;     //語文成績
    int score2;     //數學成績
    int score3;     //外語成績
    _STUDENT()
    {
        name = "";
        score1 = score2 = score3 = 0;
    }
}STUDENT,*PSTUDENT;

//班級信息
typedef struct _CLASS{
    QString name;   //班級
    QVector<STUDENT*> students;
    _CLASS()
    {
        name = "";
    }
}CLASS;
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //初始化模擬數據:學生成績
    //10個班級、每個班級10000個學生,共10W行記錄
    int nClass = 10;
    int nStudent = 10000;
    for(int i=0;i<nClass;i++)
    {
        CLASS* c = new CLASS;
        c->name = QString("class%1").arg(i);
        for(int j=0;j<nStudent;j++)
        {
            STUDENT* s = new STUDENT;
            s->name = QString("name%1").arg(j);
            s->score1 = s->score2 = s->score3 = (j%10)*10;
            c->students.append(s);
        }
        mClasses.append(c);
    }
}

其中mClasses爲存放模擬數據的變量:

QVector<CLASS*> mClasses;   //模擬數據

QStandardItemModel 常規model

void MainWindow::on_btn1_clicked()
{
    //1,QTreeView常用設置項
    QTreeView* t = ui->treeView;
//    t->setEditTriggers(QTreeView::NoEditTriggers);            //單元格不能編輯
    t->setSelectionBehavior(QTreeView::SelectRows);         //一次選中整行
    t->setSelectionMode(QTreeView::SingleSelection);        //單選,配合上面的整行就是一次選單行
//    t->setAlternatingRowColors(true);                       //每間隔一行顏色不一樣,當有qss時該屬性無效
    t->setFocusPolicy(Qt::NoFocus);                         //去掉鼠標移到單元格上時的虛線框

    //2,列頭相關設置
    t->header()->setHighlightSections(true);                //列頭點擊時字體變粗,去掉該效果
    t->header()->setDefaultAlignment(Qt::AlignCenter);      //列頭文字默認居中對齊
    t->header()->setDefaultSectionSize(100);                //默認列寬100
    t->header()->setStretchLastSection(true);               //最後一列自適應寬度
    t->header()->setSortIndicator(0,Qt::AscendingOrder);    //按第1列升序排序

    //3,構造Model
    QStringList headers;
    headers << QStringLiteral("班級/姓名")
            << QStringLiteral("語文")
            << QStringLiteral("數學")
            << QStringLiteral("外語")
            << QStringLiteral("總分")
            << QStringLiteral("平均分")
            << QStringLiteral("是否合格")
            << QStringLiteral("是否評優");
    QStandardItemModel* model = new QStandardItemModel(ui->treeView);
    model->setHorizontalHeaderLabels(headers);

    foreach (CLASS* c, mClasses)
    {
        //一級節點:班級
        QStandardItem* itemClass = new QStandardItem(c->name);
        model->appendRow(itemClass);

        foreach (STUDENT* s, c->students)
        {
            //二級節點:學生信息
            int score1 = s->score1;
            int score2 = s->score2;
            int score3 = s->score3;
            int nTotal = score1 + score2 + score3;
            int nAverage = nTotal/3;
            bool bPass = true;
            if(score1 < 60 || score2 < 60 || score3 < 60)
            {
                //任意一門課不合格則不合格
                bPass = false;
            }
            bool bGood = false;
            if(score1 >= 90 && score2 >= 90 && score3 >= 90)
            {
                //每門課都達到90分以上評優
                bGood = true;
            }

            QList<QStandardItem*> items;
            QStandardItem* item0 = new QStandardItem(s->name);
            QStandardItem* item1 = new QStandardItem(QString::number(score1));
            QStandardItem* item2 = new QStandardItem(QString::number(score2));
            QStandardItem* item3 = new QStandardItem(QString::number(score3));
            QStandardItem* item4 = new QStandardItem(QString::number(nTotal));
            QStandardItem* item5 = new QStandardItem(QString::number(nAverage));
            QStandardItem* item6 = new QStandardItem(bPass ? "合格" : "不合格");
            QStandardItem* item7 = new QStandardItem(bGood ? "優秀" : "-");
            items << item0 << item1 << item2 << item3 << item4 << item5 << item6 << item7;
            itemClass->appendRow(items);
        }
    }

    //4,應用model
    t->setModel(model);
}

自定義model

Qt提供一個基礎的model類QAbstractItemModel,前面幾種常用model也基本從此類而來。
我們寫一個自定義的TreeModel,繼承自該類,實現裏面的一些重載函數:

#include "TreeItem.h"
#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include "define.h"

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    explicit TreeModel(QStringList headers,QObject *parent = 0);
    ~TreeModel();

    //以下爲自定義model需要實現的一些虛函數,將會被Qt在查詢model數據時調用
    //headerData: 獲取表頭第section列的數據
    //data: 核心函數,獲取某個索引index的元素的各種數據
    //      role決定獲取哪種數據,常用有下面幾種:
    //      DisplayRole(默認):就是界面顯示的文本數據
    //      TextAlignmentRole:就是元素的文本對齊屬性
    //      TextColorRole、BackgroundRole:分別指文本顏色、單元格背景色
    //flags: 獲取index的一些標誌,一般不怎麼改
    //index: Qt向你的model請求一個索引爲parent的節點下面的row行column列子節點的元素,在本函數裏你需要返回該元素的正確索引
    //parent:獲取指定元素的父元素
    //rowCount: 獲取指定元素的子節點個數(下一級行數)
    //columnCount: 獲取指定元素的列數
    QVariant headerData(int section, Qt::Orientation orientation,int role = Qt::DisplayRole) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    QModelIndex index(int row, int column,const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

public:
    TreeItem *itemFromIndex(const QModelIndex &index) const;
    TreeItem *root();

private:
    QStringList mHeaders;   //表頭內容
    TreeItem *mRootItem;    //根節點
};

這些函數基本作用在註釋內註明了,主要需要根據自己的情況寫好data函數,其它的內容可以參考我的示例代碼,略微調整。
其中TreeItem 爲我們自定義的指代一個節點的類:

#include <QVariant>

class TreeItem
{
public:
    explicit TreeItem(TreeItem *parentItem = 0);
    ~TreeItem();

    void appendChild(TreeItem *child);      //在本節點下增加子節點
    void removeChilds();                    //清空所有節點

    TreeItem *child(int row);               //獲取第row個子節點指針
    TreeItem *parentItem();                 //獲取父節點指針
    int childCount() const;                 //子節點計數
    int row() const;                        //獲取該節點是父節點的第幾個子節點

    //核心函數:獲取節點第column列的數據
    QVariant data(int column) const;

    //設置、獲取節點是幾級節點(就是樹的層級)
    int level(){ return mLevel; }
    void setLevel(int level){ mLevel = level; }

    //設置、獲取節點存的數據指針
    void setPtr(void* p){ mPtr = p; }
    void* ptr(){ return mPtr; }

    //保存該節點是其父節點的第幾個子節點,查詢優化所用
    void setRow(int row){
        mRow = row;
    }

private:
    QList<TreeItem*> mChildItems;   //子節點
    TreeItem *mParentItem;          //父節點
    int mLevel;     //該節點是第幾級節點
    void* mPtr;     //存儲數據的指針
    int mRow;       //記錄該item是第幾個,可優化查詢效率

};

其中只需存一個真實數據的指針void* mPtr 即可,這樣便大大減少了因爲常規Model內重複存儲數據所帶來的內存開銷,這也是該方法能節約內存的主要原因。

另外介紹幾個核心函數實現:
TreeModel::data():視圖獲取數據時調用的函數,裏面通過具體的TreeItem::data()獲取最終數據

QVariant TreeModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
    if (role == Qt::DisplayRole)
    {
        return item->data(index.column());
    }
    return QVariant();
}
QVariant TreeItem::data(int column) const
{
    if(mLevel == 1)
    {
        //一級節點,班級
        if(column == 0)
        {
            CLASS* c = (CLASS*)mPtr;
            return c->name;
        }
    }
    else if(mLevel==2)
    {
        //二級節點學生信息
        STUDENT* s = (STUDENT*)mPtr;
        switch (column)
        {
        case 0: return s->name;
        case 1: return QString::number(s->score1);
        case 2: return QString::number(s->score2);
        case 3: return QString::number(s->score3);
        case 4: return QString::number(s->score1 + s->score2 + s->score3);
        case 5: return QString::number( (s->score1 + s->score2 + s->score3)/3 );
        case 6:
        {
            if(s->score1 < 60 || s->score2 < 60 || s->score3 < 60)
            {
                //任意一門課不合格則不合格
                return QStringLiteral("不合格");
            }
            else
            {
                return QStringLiteral("合格");
            }
        }
        case 7:
        {
            if(s->score1 >= 90 && s->score2 >= 90 && s->score3 >= 90)
            {
                //每門課都達到90分以上評優
                return QStringLiteral("優秀");
            }
            else
            {
                return QStringLiteral("-");
            }
        }
        default:
            return QVariant();
        }
    }
    return QVariant();
}

看到這裏,可以發現,自定義model實際需要存儲的數據,比界面所顯示的列數要少的多!
只要能通過現有數據推算出來的列的數據,都可以不存儲!
比如我們只存儲了基本的3門課程分數,其他內容全爲顯示時視圖向我們的自定義model獲取數據時實時計算得出的!

可能你會擔心,這樣計算量會不會變大,導致反應速度變慢?
其實視圖只會對當前需要顯示的數據來請求,意思就是,無論總數據多少,只對當前可見的內容進行計算,你想想電腦屏幕就那麼大,這個計算量簡直毫無壓力。

因此,由於實際需要存儲列數變少,內存佔用又得到可觀的縮減。
不過這種好處只適用於多列的數據有關聯可推算的情況。
我的項目內存在大量此類數據,獲得收益較大。

進一步瞭解可以閱讀源碼。

源碼

TreeDemo13 自定義model示例
源碼我使用Qt5.8.0 mingw版本在win7編譯通過,如有編譯問題嘗試使用相同環境試試。

原文地址:https://blog.csdn.net/dpsying/article/details/80456263

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章