前面我們說了 Q t提供的幾個預定義 model。但是,面對變化萬千的需求,那幾個 model 是遠遠不能滿足我們的需要的。另外,對于 Qt 這種框架來說,model 的選擇首先要能滿足絕大多數(shù)功能的需要,這就是說,可能這個model中的某些功能你永遠也不會用到,但是還要帶著它,這樣做的后果就是效率不會很高。所以,我們還必須要能夠自定義 model。
在我們真正的完成自定義 model 之前,先來看看在 Qt 的 model-view 架構中的幾個關鍵的概念。一個 model 中的每個數(shù)據(jù)元素都有一個 model 索引。這個索引指明這個數(shù)據(jù)位于 model 的位置,比如行、列等。這就是前面我們曾經(jīng)說到過的 QModelIndex。每個數(shù)據(jù)元素還要有一組屬性值,稱為角色(roles)。這個屬性值并不是數(shù)據(jù)的內(nèi)容,而是它的屬性,比如說,這個數(shù)據(jù)是用來展示數(shù)據(jù)的,還是用于顯示列頭的?因此,這組屬性值實際上是 Qt 的一個 enum 定義的,比較常見的有 Qt::DisplayRole 和 Qt::EditRole,另外還有 Qt::ToolTipRole, Qt::StatusTipRole, 和Qt::WhatsThisRole 等。并且,還有一些屬性是用來描述基本的展現(xiàn)屬性的,比如 Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole, Qt::BackgroundColorRole 等。
對于 list model 而言,要定位其中的一個數(shù)據(jù)只需要有一個行號就可以了,這個行號可以通過QModelIndex::row()函數(shù)進行訪問;對于 table model 而言,這種定位需要有兩個值:行號和列號,這兩個值可以通過 QModelIndex::row()和 QModelIndex::column()這兩個函數(shù)訪問到。另外,對于 tree model 而言,用于定位的可以是這個元素的父節(jié)點。實際上,不僅僅是 tree model,并且 list model 和 table model 的元素也都有自己的父節(jié)點,只不過對于 list model 和 table model,它們元素的父節(jié)點都是相同的,并且指向一個非法的 QModelIndex。對于所有的model,這個父節(jié)點都可以通過 QModelIndex::parent()函數(shù)訪問到。這就是說,每個 model 的項都有自己的角色數(shù)據(jù),0個、1個或多個子節(jié)點。既然每個元素都有自己的子元素,那么它們就可以通過遞歸的算法進行遍歷,就像數(shù)據(jù)結構中樹的遍歷一樣。關于父節(jié)點的描述,請看下面這張圖(出自 C++ GUI Programming with Qt4, 2nd Edition):
下面我們通過一個簡單的例子來看看如何實現(xiàn)自定義 model。這個例子來自 C++ GUI Programming with Qt4, 2nd Edition。首先描述一下需求。這里我們要實現(xiàn)的是一個類似于貨幣匯率表的 table?;蛟S你會想,這是一個很簡單的實現(xiàn),直接用 QTableWidget 不就可以了嗎?的確,如果直接使用 QTableWidget 確實很方便。但是,試想一個包含了100種貨幣的匯率表。顯然,這是一個二維表,并且,對于每一種貨幣,都需要給出相對于其他100種貨幣的匯率(在這里,我們把自己對自己的匯率也包含在內(nèi),只不過這個匯率永遠是1.0000)。那么,這張表要有100 x 100 = 10000個數(shù)據(jù)項?,F(xiàn)在要求我們減少存儲空間。于是我們想,如果我們的數(shù)據(jù)不是顯示的數(shù)據(jù),而是這種貨幣相對于美元的匯率,那么,其他貨幣的匯率都可以根據(jù)這個匯率計算出來了。比如說,我存儲的是人民幣相對美元的匯率,日元相對美元的匯率,那么人民幣相對日元的匯率只要作一下比就可以得到了。我沒有必要存儲10000個數(shù)據(jù)項,只要存儲100個就夠了。于是,我們要自己實現(xiàn)一個 model。
CurrencyModel 就是這樣一個 model。它底層的數(shù)據(jù)使用一個 QMap<QString, double>類型的數(shù)據(jù),作為 key 的 QString 是貨幣名字,作為 value 的 double 是這種貨幣對美元的匯率。然后我們來看代碼:
.h
class CurrencyModel : public QAbstractTableModel
{
public:
CurrencyModel(QObject *parent = 0);
void setCurrencyMap(const QMap<QString, double> &map);
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
private:
QString currencyAt(int offset) const;
QMap<QString, double> currencyMap;
};
.cpp
CurrencyModel::CurrencyModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
int CurrencyModel::rowCount(const QModelIndex & parent) const
{
return currencyMap.count();
}
int CurrencyModel::columnCount(const QModelIndex & parent) const
{
return currencyMap.count();
}
QVariant CurrencyModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (role == Qt::TextAlignmentRole) {
return int(Qt::AlignRight | Qt::AlignVCenter);
} else if (role == Qt::DisplayRole) {
QString rowCurrency = currencyAt(index.row());
QString columnCurrency = currencyAt(index.column());
if (currencyMap.value(rowCurrency) == 0.0)
return "####";
double amount = currencyMap.value(columnCurrency) / currencyMap.value(rowCurrency);
return QString("%1").arg(amount, 0, 'f', 4);
}
return QVariant();
}
QVariant CurrencyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
return currencyAt(section);
}
void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map)
{
currencyMap = map;
reset();
}
QString CurrencyModel::currencyAt(int offset) const
{
return (currencyMap.begin() + offset).key();
}
我們選擇了繼承 QAbstractTableModel。雖然是自定義 model,但各種 model 之間也會有很多共性。Qt 提供了一系列的抽象類供我們繼承,以便讓我們只需要覆蓋掉幾個函數(shù)就可以輕松地定義出我們自己的 model。Qt 提供了 QAbstractListModel 和 QAbstractTableModel 兩類,前者是一維數(shù)據(jù) model,后者是二維數(shù)據(jù) model。如果你的數(shù)據(jù)很復雜,那么可以直接繼承 QAbstractItemModel。這三個類之間的關系可以表述如下:(出自 C++ GUI Programming with Qt4, 2nd Edition):
構造函數(shù)中沒有添加任何代碼,只要調(diào)用父類的構造函數(shù)就可以了。然后我們重寫了 rowCount()和columnCount()這兩個函數(shù),用于返回 model 的行數(shù)和列數(shù)。由于我們使用一維的 map 記錄數(shù)據(jù),因此這里的行和列都是 map 的大小。然后我們看最復雜的 data()函數(shù)。
QVariant CurrencyModel::data(const QModelIndex &index, int role) const{if (!index.isValid())return QVariant();
if (role == Qt::TextAlignmentRole) {
return int(Qt::AlignRight | Qt::AlignVCenter);
} else if (role == Qt::DisplayRole) {
QString rowCurrency = currencyAt(index.row());
QString columnCurrency = currencyAt(index.column());
if (currencyMap.value(rowCurrency) == 0.0)
return "####";
double amount = currencyMap.value(columnCurrency) / currencyMap.value(rowCurrency);
return QString("%1").arg(amount, 0, 'f', 4);
}
return QVariant();
}
data()函數(shù)返回單元格的數(shù)據(jù)。它有兩個參數(shù):第一個是 QModelIndex,也就是單元格的位置;第二個是 role,也就是這個數(shù)據(jù)的角色。這個函數(shù)的返回值是 QVariant。至此,我們還是第一次見到這個類型。這個類型相當于是 Java 里面的 Object,它把絕大多數(shù) Qt 提供的數(shù)據(jù)類型都封裝起來,起到一個數(shù)據(jù)類型“擦除”的作用。比如我們的 table 單元格可以是 string,也可以是 int,也可以是一個顏色值,那么這么多類型怎么返回呢?于是,Qt 提供了這個 QVariant 類型,你可以把這很多類型都存放進去,到需要使用的時候使用一系列的 to 函數(shù)取出來即可。比如你把 int 包裝成一個QVariant,使用的時候要用 QVariant::toInt()重新取出來。這里需要注意的是,QVariant 類型的放入和取出必須是相對應的,你放入一個 int 就必須按 int 取出,不能用 toString(), Qt 不會幫你自動轉換?;蛟S你會問,Qt 不是提供了一個 QObject 類型嗎?為什么不像 Java 一樣都用 Object呢?關于這一點豆子也沒有官方文檔,不過可以猜測一下。和 Java 不同,C++的面向?qū)ο篌w系不是單根的,C++對象并不是都繼承于某一個類,因此,如果你要實現(xiàn)一個這種功能的類,做到“類型擦除”,就必須用一個類包含所有的數(shù)據(jù)類型。就相當于設計一個能放進所有形狀的盒子,你才能把各種各樣的形狀放進去。這樣的話這個類就會變得異常龐大。對于 Qt,QObject 類是大多數(shù)類繼承的類,理應越小越好,因此就把這個功能抽取出來,形成了一個新類。這也只是豆子的猜測,大家不必往心里去:-)
好了,下面看這個類的內(nèi)容。首先判斷傳入的 index 是不是合法,如果不合法直接 return 一個空白的 QVariant。然后如果 role 是 Qt::TextAlignmentRole,也就是文本的對象方式,那么就返回int(Qt::AlignRight | Qt::AlignVCenter);否則,role 如果是 Qt::DisplayRole,就按照我們前面所說的邏輯進行計算,然后按照字符串返回。這時候你就會發(fā)現(xiàn),其實我們在 if…else…里面返回的不是一種數(shù)據(jù)類型,if 里面是 int,而 else 里面是 QString,這就是 QVariant 的作用了,也正是“類型擦除”的意思。
剩下的三個函數(shù)就很簡單了:headerData()返回列名或者行名;setCurrencyMap()用于設置底層的數(shù)據(jù)源;currencyAt()返回偏移量為 offset 的鍵值。
至于調(diào)用就很簡單了:CurrencyTable::CurrencyTable(){QMap<QString, double> data;data["NOK"] = 1.0000;data["NZD"] = 0.2254;data["SEK"] = 1.1991;data["SGD"] = 0.2592;data["USD"] = 0.1534;
CurrencyModel *model = new CurrencyModel;
model->setCurrencyMap(data);
QTableView *view = new QTableView(this);
view->setModel(model);
view->resize(400, 300);
}
好了,最后讓我們來看一下最終結果吧!
注意,這一章中的代碼不是完整的代碼,缺少 view 的頭文件,不過這只是一個空白的文件。你也可以直接把 view 的代碼放到 main()函數(shù)里面運行。
本文出自 “豆子空間” 博客,請務必保留此出處 http://devbean.blog.51cto.com/448512/193918
更多建議: