關於model的用法。。。

【轉自】:http://www.uplook.cn/index-Index-show-view1690.html?treeid=624#

前面我們說了Qt提供的幾個預定義model。但是,面對變化萬千的需求,那幾個model是遠遠不能滿足我們的需要的。另外,對於Qt這種框架來說,model的選擇首先要能滿足絕大多數功能的需要,這就是說,可能這個model中的某些功能你永遠也不會用到,但是還要帶着它,這樣做的後果就是效率不會很高。所以,我們還必須要能夠自定義model。
 
在我們真正的完成自定義model之前,先來看看在Qt的model-view架構中的幾個關鍵的概念。一個model中的每個數據元素都有一個model索引。這個索引指明這個數據位於model的位置,比如行、列等。這就是前面我們曾經說到過的QModelIndex。每個數據元素還要有一組屬性值,稱爲角色(roles)。這個屬性值並不是數據的內容,而是它的屬性,比如說,這個數據是用來展示數據的,還是用於顯示列頭的?因此,這組屬性值實際上是Qt的一個enum定義的,比較常見的有Qt::DisplayRole和Qt::EditRole,另外還有Qt::ToolTipRole, Qt::StatusTipRole, 和Qt::WhatsThisRole等。並且,還有一些屬性是用來描述基本的展現屬性的,比如Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole, Qt::BackgroundColorRole等。
 
對於list model而言,要定位其中的一個數據只需要有一個行號就可以了,這個行號可以通過QModelIndex::row()函數進行訪問;對於table model而言,這種定位需要有兩個值:行號和列號,這兩個值可以通過QModelIndex::row()和QModelIndex::column()這兩個函數訪問到。另外,對於tree model而言,用於定位的可以是這個元素的父節點。實際上,不僅僅是tree model,並且list model和table model的元素也都有自己的父節點,只不過對於list model和table model,它們元素的父節點都是相同的,並且指向一個非法的QModelIndex。對於所有的model,這個父節點都可以通過QModelIndex::parent()函數訪問到。這就是說,每個model的項都有自己的角色數據,0個、1個或多個子節點。既然每個元素都有自己的子元素,那麼它們就可以通過遞歸的算法進行遍歷,就像數據結構中樹的遍歷一樣。關於父節點的描述,請看下面這張圖(出自C++ GUI Programming with Qt4, 2nd Edition):
下面我們通過一個簡單的例子來看看如何實現自定義model。這個例子來自C++ GUI Programming with Qt4, 2nd Edition。首先描述一下需求。這裏我們要實現的是一個類似於貨幣匯率表的table。或許你會想,這是一個很簡單的實現,直接用QTableWidget不就可以了嗎?的確,如果直接使用QTableWidget確實很方便。但是,試想一個包含了100種貨幣的匯率表。顯然,這是一個二維表,並且,對於每一種貨幣,都需要給出相對於其他100種貨幣的匯率(在這裏,我們把自己對自己的匯率也包含在內,只不過這個匯率永遠是1.0000)。那麼,這張表要有100 x 100 = 10000個數據項。現在要求我們減少存儲空間。於是我們想,如果我們的數據不是顯示的數據,而是這種貨幣相對於美元的匯率,那麼,其他貨幣的匯率都可以根據這個匯率計算出來了。比如說,我存儲的是人民幣相對美元的匯率,日元相對美元的匯率,那麼人民幣相對日元的匯率只要作一下比就可以得到了。我沒有必要存儲10000個數據項,只要存儲100個就夠了。於是,我們要自己實現一個model。
 
CurrencyModel就是這樣一個model。它底層的數據使用一個QMap<QString, double>類型的數據,作爲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提供了一系列的抽象類供我們繼承,以便讓我們只需要覆蓋掉幾個函數就可以輕鬆地定義出我們自己的model。Qt提供了QAbstractListModel和QAbstractTableModel兩類,前者是一維數據model,後者是二維數據model。如果你的數據很複雜,那麼可以直接繼承QAbstractItemModel。這三個類之間的關係可以表述如下:(出自C++ GUI Programming with Qt4, 2nd Edition):
 
2010-1-17
構造函數中沒有添加任何代碼,只要調用父類的構造函數就可以了。然後我們重寫了rowCount()和columnCount()這兩個函數,用於返回model的行數和列數。由於我們使用一維的map記錄數據,因此這裏的行和列都是map的大小。然後我們看最複雜的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。至此,我們還是第一次見到這個類型。這個類型相當於是Java裏面的Object,它把絕大多數Qt提供的數據類型都封裝起來,起到一個數據類型“擦除”的作用。比如我們的table單元格可以是string,也可以是int,也可以是一個顏色值,那麼這麼多類型怎麼返回呢?於是,Qt提供了這個QVariant類型,你可以把這很多類型都存放進去,到需要使用的時候使用一系列的to函數取出來即可。比如你把int包裝成一個QVariant,使用的時候要用QVariant::toInt()重新取出來。這裏需要注意的是,QVariant類型的放入和取出必須是相對應的,你放入一個int就必須按int取出,不能用toString(), Qt不會幫你自動轉換。或許你會問,Qt不是提供了一個QObject類型嗎?爲什麼不像Java一樣都用Object呢?關於這一點豆子也沒有官方文檔,不過可以猜測一下。和Java不同,C++的面向對象體系不是單根的,C++對象並不是都繼承於某一個類,因此,如果你要實現一個這種功能的類,做到“類型擦除”,就必須用一個類包含所有的數據類型。就相當於設計一個能放進所有形狀的盒子,你才能把各種各樣的形狀放進去。這樣的話這個類就會變得異常龐大。對於Qt,QObject類是大多數類繼承的類,理應越小越好,因此就把這個功能抽取出來,形成了一個新類。這也只是豆子的猜測,大家不必往心裏去:-)
 
好了,下面看這個類的內容。首先判斷傳入的index是不是合法,如果不合法直接return一個空白的QVariant。然後如果role是Qt::TextAlignmentRole,也就是文本的對象方式,來判斷靠右邊停靠,並且豎直方向上居中,那麼就返回int(Qt::AlignRight | Qt::AlignVCenter);否則,role如果是Qt::DisplayRole,就按照我們前面所說的邏輯進行計算,然後按照字符串返回。這時候你就會發現,其實我們在if…else…裏面返回的不是一種數據類型,if裏面是int,而else裏面是QString,這就是QVariant的作用了,也正是“類型擦除”的意思。
 
剩下的三個函數就很簡單了:headerData()返回列名或者行名;setCurrencyMap()用於設置底層的數據源;currencyAt()返回偏移量爲offset的鍵值。
 
至於調用就很簡單了:
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); 
}
 
好了,最後讓我們來看一下最終結果吧!
 
2010-1-17
 
注意,這一章中的代碼不是完整的代碼,缺少view的頭文件,不過這只是一個空白的文件。你也可以直接把view的代碼放到main()函數裏面運行。
發佈了10 篇原創文章 · 獲贊 7 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章