簡述
前面簡單介紹過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的數倍甚至數十倍!數據量越大內存差距越明顯。
這裏以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編譯通過,如有編譯問題嘗試使用相同環境試試。