#(49):自定義只讀模型
model/view 模型將數據與視圖分割開來,也就是說,我們可以為不同的視圖,`QListView`、`QTableView`和`QTreeView`提供一個數據模型,這樣我們可以從不同角度來展示數據的方方面面。但是,面對變化萬千的需求,Qt 預定義的幾個模型是遠遠不能滿足需要的。因此,我們還必須自定義模型。
類似`QAbstractView`類之于自定義視圖,`QAbstractItemModel`?為自定義模型提供了一個足夠靈活的接口。它能夠支持數據源的層次結構,能夠對數據進行增刪改操作,還能夠支持拖放。不過,有時候一個靈活的類往往顯得過于復雜,所以,Qt 又提供了`QAbstarctListModel`和`QAbstractTableModel`兩個類來簡化非層次數據模型的開發。顧名思義,這兩個類更適合于結合列表和表格使用。
本節,我們正式開始對自定義模型進行介紹。
在開始自定義模型之前,我們首先需要思考這樣一個問題:我們的數據結構適合于哪種視圖的顯示方式?是列表,還是表格,還是樹?如果我們的數據僅僅用于列表或表格的顯示,那么`QAbstractListModel`或者`QAbstractTableModel`?已經足夠,它們為我們實現了很多默認函數。但是,如果我們的數據具有層次結構,并且必須向用戶顯示這種層次,我們只能選擇`QAbstractItemModel`。不管底層數據結構是怎樣的格式,最好都要直接考慮適應于標準的`QAbstractItemModel`的接口,這樣就可以讓更多視圖能夠輕松訪問到這個模型。
現在,我們開始自定義一個模型。這個例子修改自《C++ GUI Programming with Qt4, 2nd Edition》。首先描述一下需求。我們想要實現的是一個貨幣匯率表,就像銀行營業廳墻上掛著的那種電子公告牌。當然,你可以選擇`QTableWidget`。的確,直接使用`QTableWidget`確實很方便。但是,試想一個包含了 100 種貨幣的匯率表。顯然,這是一個二維表,并且對于每一種貨幣,都需要給出相對于其他 100 種貨幣的匯率(我們把自己對自己的匯率也包含在內,只不過這個匯率永遠是 1.0000)。現在,按照我們的設計,這張表要有 100 x 100 = 10000 個數據項。我們希望減少存儲空間,有沒有更好的方式?于是我們想,如果我們的數據不是直接向用戶顯示的數據,而是這種貨幣相對于美元的匯率,那么其它貨幣的匯率都可以根據這個匯率計算出來了。比如,我存儲人民幣相對美元的匯率,日元相對美元的匯率,那么人民幣相對日元的匯率只要作一下比就可以得到了。這種數據結構就沒有必要存儲 10000 個數據項,只要存儲 100 個就夠了(實際情況中這可能是不現實的,因為兩次運算會帶來更大的誤差,但這不在我們現在的考慮范疇中)。
于是我們設計了`CurrencyModel`類。它底層使用`QMap<QString, double>`數據結構進行存儲,`QString`類型的鍵是貨幣名字,`double`類型的值是這種貨幣相對美元的匯率。(這里提一點,實際應用中,永遠不要使用 double 處理金額敏感的數據!因為 double 是不精確的,不過這一點顯然不在我們的考慮中。)
首先從頭文件開始看起:
~~~
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;
};
~~~
這段代碼平淡無奇,我們繼承了`QAbstractTableModel`類,然后重寫了所要求的幾個函數。構造函數同樣如此:
~~~
CurrencyModel::CurrencyModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
~~~
`rowCount()`和`columnCount()`用于返回行和列的數目。記得我們保存的是每種貨幣相對美元的匯率,而需要顯示的是它們兩兩之間的匯率,因此這兩個函數都應該返回這個 map 的項數:
~~~
int CurrencyModel::rowCount(const QModelIndex & parent) const
{
return currencyMap.count();
}
int CurrencyModel::columnCount(const QModelIndex & parent) const
{
return currencyMap.count();
}
~~~
`headerData()`用于返回列名:
~~~
QVariant CurrencyModel::headerData(int section, Qt::Orientation, int role) const
{
if (role != Qt::DisplayRole) {
return QVariant();
}
return currencyAt(section);
}
~~~
我們在前面的章節中介紹過有關角色的概念。這里我們首先判斷這個角色是不是用于顯示的,如果是,則調用`currencyAt()`函數返回第 section 列的名字;如果不是則返回一個空白的`QVariant`對象。`currencyAt()`函數定義如下:
~~~
QString CurrencyModel::currencyAt(int offset) const
{
return (currencyMap.begin() + offset).key();
}
~~~
如果不了解`QVariant`類,可以簡單認為這個類型相當于 Java 里面的 Object,它把 Qt 提供的大部分數據類型封裝起來,起到一個類型擦除的作用。比如我們的單元格的數據可以是 string,可以是 int,也可以是一個顏色值,這么多類型怎么使用一個函數返回呢?回憶一下,返回值并不用于區分一個函數。于是,Qt 提供了`QVariant`類型。你可以把很多類型存放進去,到需要使用的時候使用一系列的 to 函數取出來即可。比如把 int 包裝成一個 QVariant,使用的時候要用`QVariant::toInt()`重新取出來。這非常類似于 union,但是 union 的問題是,無法保持沒有默認構造函數的類型,于是 Qt 提供了`QVariant`作為 union 的一種模擬。
`setCurrencyMap()`函數則是用于設置底層的實際數據。由于我們不可能將這種數據硬編碼,所以我們必須為模型提供一個用于設置的函數:
~~~
void CurrencyModel::setCurrencyMap(const QMap<QString, double> &map)
{
beginResetModel();
currencyMap = map;
endResetModel();
}
~~~
我們當然可以直接設置 currencyMap,但是我們依然添加了`beginResetModel()`和`endResetModel()`兩個函數調用。這將告訴關心這個模型的其它類,現在要重置內部數據,大家要做好準備。這是一種契約式的編程方式。
接下來便是最復雜的`data()`函數:
~~~
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()`函數返回一個單元格的數據。它有兩個參數:第一個是`QModelIndex`,也就是單元格的位置;第二個是`role`,也就是這個數據的角色。這個函數的返回值是`QVariant`類型。我們首先判斷傳入的`index`是不是合法,如果不合法直接返回一個空白的`QVariant`。然后如果`role`是`Qt::TextAlignmentRole`,也就是文本的對齊方式,返回`int(Qt::AlignRight | Qt::AlignVCenter)`;如果是`Qt::DisplayRole`,就按照我們前面所說的邏輯進行計算,然后以字符串的格式返回。這時候你就會發現,其實我們在 if…else… 里面返回的不是一種數據類型:if 里面返回的是 int,而 else 里面是`QString`,這就是`QVariant`的作用了。
為了看看實際效果,我們可以使用這樣的`main()`函數代碼:
~~~
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QMap<QString, double> data;
data["USD"] = 1.0000;
data["CNY"] = 0.1628;
data["GBP"] = 1.5361;
data["EUR"] = 1.2992;
data["HKD"] = 0.1289;
QTableView view;
CurrencyModel *model = new CurrencyModel(&view);
model->setCurrencyMap(data);
view.setModel(model);
view.resize(400, 300);
view.show();
return a.exec();
}
~~~
這是我們的實際運行效果:
[](http://files.devbean.net/images/2013/05/custom-model-r.png)
自定義只讀模型
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)