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 是不精確的,不過這一點顯然不在咱們的考慮中。)code
首先從頭文件開始看起:對象
#ifndef CURRENCYMODEL_H #define CURRENCYMODEL_H #include <QWidget> #include <QAbstractTableModel> #include <QMap> 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; }; #endif // CURRENCYMODEL_H
這段代碼平淡無奇,咱們繼承了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(); }
這是咱們的實際運行效果: