Qt 自定義 model 之一

2018-10-08 09:44 更新

自定義 model 之一

前面我們說了 Q t提供的幾個(gè)預(yù)定義 model。但是,面對(duì)變化萬千的需求,那幾個(gè) model 是遠(yuǎn)遠(yuǎn)不能滿足我們的需要的。另外,對(duì)于 Qt 這種框架來說,model 的選擇首先要能滿足絕大多數(shù)功能的需要,這就是說,可能這個(gè)model中的某些功能你永遠(yuǎn)也不會(huì)用到,但是還要帶著它,這樣做的后果就是效率不會(huì)很高。所以,我們還必須要能夠自定義 model。

在我們真正的完成自定義 model 之前,先來看看在 Qt 的 model-view 架構(gòu)中的幾個(gè)關(guān)鍵的概念。一個(gè) model 中的每個(gè)數(shù)據(jù)元素都有一個(gè) model 索引。這個(gè)索引指明這個(gè)數(shù)據(jù)位于 model 的位置,比如行、列等。這就是前面我們?cè)?jīng)說到過的 QModelIndex。每個(gè)數(shù)據(jù)元素還要有一組屬性值,稱為角色(roles)。這個(gè)屬性值并不是數(shù)據(jù)的內(nèi)容,而是它的屬性,比如說,這個(gè)數(shù)據(jù)是用來展示數(shù)據(jù)的,還是用于顯示列頭的?因此,這組屬性值實(shí)際上是 Qt 的一個(gè) enum 定義的,比較常見的有 Qt::DisplayRole 和 Qt::EditRole,另外還有 Qt::ToolTipRole, Qt::StatusTipRole, 和Qt::WhatsThisRole 等。并且,還有一些屬性是用來描述基本的展現(xiàn)屬性的,比如 Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole, Qt::BackgroundColorRole 等。

對(duì)于 list model 而言,要定位其中的一個(gè)數(shù)據(jù)只需要有一個(gè)行號(hào)就可以了,這個(gè)行號(hào)可以通過QModelIndex::row()函數(shù)進(jìn)行訪問;對(duì)于 table model 而言,這種定位需要有兩個(gè)值:行號(hào)和列號(hào),這兩個(gè)值可以通過 QModelIndex::row()和 QModelIndex::column()這兩個(gè)函數(shù)訪問到。另外,對(duì)于 tree model 而言,用于定位的可以是這個(gè)元素的父節(jié)點(diǎn)。實(shí)際上,不僅僅是 tree model,并且 list model 和 table model 的元素也都有自己的父節(jié)點(diǎn),只不過對(duì)于 list model 和 table model,它們?cè)氐母腹?jié)點(diǎn)都是相同的,并且指向一個(gè)非法的 QModelIndex。對(duì)于所有的model,這個(gè)父節(jié)點(diǎn)都可以通過 QModelIndex::parent()函數(shù)訪問到。這就是說,每個(gè) model 的項(xiàng)都有自己的角色數(shù)據(jù),0個(gè)、1個(gè)或多個(gè)子節(jié)點(diǎn)。既然每個(gè)元素都有自己的子元素,那么它們就可以通過遞歸的算法進(jìn)行遍歷,就像數(shù)據(jù)結(jié)構(gòu)中樹的遍歷一樣。關(guān)于父節(jié)點(diǎn)的描述,請(qǐng)看下面這張圖(出自 C++ GUI Programming with Qt4, 2nd Edition):

下面我們通過一個(gè)簡(jiǎn)單的例子來看看如何實(shí)現(xiàn)自定義 model。這個(gè)例子來自 C++ GUI Programming with Qt4, 2nd Edition。首先描述一下需求。這里我們要實(shí)現(xiàn)的是一個(gè)類似于貨幣匯率表的 table?;蛟S你會(huì)想,這是一個(gè)很簡(jiǎn)單的實(shí)現(xiàn),直接用 QTableWidget 不就可以了嗎?的確,如果直接使用 QTableWidget 確實(shí)很方便。但是,試想一個(gè)包含了100種貨幣的匯率表。顯然,這是一個(gè)二維表,并且,對(duì)于每一種貨幣,都需要給出相對(duì)于其他100種貨幣的匯率(在這里,我們把自己對(duì)自己的匯率也包含在內(nèi),只不過這個(gè)匯率永遠(yuǎn)是1.0000)。那么,這張表要有100 x 100 = 10000個(gè)數(shù)據(jù)項(xiàng)?,F(xiàn)在要求我們減少存儲(chǔ)空間。于是我們想,如果我們的數(shù)據(jù)不是顯示的數(shù)據(jù),而是這種貨幣相對(duì)于美元的匯率,那么,其他貨幣的匯率都可以根據(jù)這個(gè)匯率計(jì)算出來了。比如說,我存儲(chǔ)的是人民幣相對(duì)美元的匯率,日元相對(duì)美元的匯率,那么人民幣相對(duì)日元的匯率只要作一下比就可以得到了。我沒有必要存儲(chǔ)10000個(gè)數(shù)據(jù)項(xiàng),只要存儲(chǔ)100個(gè)就夠了。于是,我們要自己實(shí)現(xiàn)一個(gè) model。

CurrencyModel 就是這樣一個(gè) model。它底層的數(shù)據(jù)使用一個(gè) QMap<QString, double>類型的數(shù)據(jù),作為 key 的 QString 是貨幣名字,作為 value 的 double 是這種貨幣對(duì)美元的匯率。然后我們來看代碼:

.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 之間也會(huì)有很多共性。Qt 提供了一系列的抽象類供我們繼承,以便讓我們只需要覆蓋掉幾個(gè)函數(shù)就可以輕松地定義出我們自己的 model。Qt 提供了 QAbstractListModel 和 QAbstractTableModel 兩類,前者是一維數(shù)據(jù) model,后者是二維數(shù)據(jù) model。如果你的數(shù)據(jù)很復(fù)雜,那么可以直接繼承 QAbstractItemModel。這三個(gè)類之間的關(guān)系可以表述如下:(出自 C++ GUI Programming with Qt4, 2nd Edition):

構(gòu)造函數(shù)中沒有添加任何代碼,只要調(diào)用父類的構(gòu)造函數(shù)就可以了。然后我們重寫了 rowCount()和columnCount()這兩個(gè)函數(shù),用于返回 model 的行數(shù)和列數(shù)。由于我們使用一維的 map 記錄數(shù)據(jù),因此這里的行和列都是 map 的大小。然后我們看最復(fù)雜的 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ù)。它有兩個(gè)參數(shù):第一個(gè)是 QModelIndex,也就是單元格的位置;第二個(gè)是 role,也就是這個(gè)數(shù)據(jù)的角色。這個(gè)函數(shù)的返回值是 QVariant。至此,我們還是第一次見到這個(gè)類型。這個(gè)類型相當(dāng)于是 Java 里面的 Object,它把絕大多數(shù) Qt 提供的數(shù)據(jù)類型都封裝起來,起到一個(gè)數(shù)據(jù)類型“擦除”的作用。比如我們的 table 單元格可以是 string,也可以是 int,也可以是一個(gè)顏色值,那么這么多類型怎么返回呢?于是,Qt 提供了這個(gè) QVariant 類型,你可以把這很多類型都存放進(jìn)去,到需要使用的時(shí)候使用一系列的 to 函數(shù)取出來即可。比如你把 int 包裝成一個(gè)QVariant,使用的時(shí)候要用 QVariant::toInt()重新取出來。這里需要注意的是,QVariant 類型的放入和取出必須是相對(duì)應(yīng)的,你放入一個(gè) int 就必須按 int 取出,不能用 toString(), Qt 不會(huì)幫你自動(dòng)轉(zhuǎn)換?;蛟S你會(huì)問,Qt 不是提供了一個(gè) QObject 類型嗎?為什么不像 Java 一樣都用 Object呢?關(guān)于這一點(diǎn)豆子也沒有官方文檔,不過可以猜測(cè)一下。和 Java 不同,C++的面向?qū)ο篌w系不是單根的,C++對(duì)象并不是都繼承于某一個(gè)類,因此,如果你要實(shí)現(xiàn)一個(gè)這種功能的類,做到“類型擦除”,就必須用一個(gè)類包含所有的數(shù)據(jù)類型。就相當(dāng)于設(shè)計(jì)一個(gè)能放進(jìn)所有形狀的盒子,你才能把各種各樣的形狀放進(jìn)去。這樣的話這個(gè)類就會(huì)變得異常龐大。對(duì)于 Qt,QObject 類是大多數(shù)類繼承的類,理應(yīng)越小越好,因此就把這個(gè)功能抽取出來,形成了一個(gè)新類。這也只是豆子的猜測(cè),大家不必往心里去:-)

好了,下面看這個(gè)類的內(nèi)容。首先判斷傳入的 index 是不是合法,如果不合法直接 return 一個(gè)空白的 QVariant。然后如果 role 是 Qt::TextAlignmentRole,也就是文本的對(duì)象方式,那么就返回int(Qt::AlignRight | Qt::AlignVCenter);否則,role 如果是 Qt::DisplayRole,就按照我們前面所說的邏輯進(jìn)行計(jì)算,然后按照字符串返回。這時(shí)候你就會(huì)發(fā)現(xiàn),其實(shí)我們?cè)?if…else…里面返回的不是一種數(shù)據(jù)類型,if 里面是 int,而 else 里面是 QString,這就是 QVariant 的作用了,也正是“類型擦除”的意思。

剩下的三個(gè)函數(shù)就很簡(jiǎn)單了:headerData()返回列名或者行名;setCurrencyMap()用于設(shè)置底層的數(shù)據(jù)源;currencyAt()返回偏移量為 offset 的鍵值。

至于調(diào)用就很簡(jiǎn)單了: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); 

}

好了,最后讓我們來看一下最終結(jié)果吧!

注意,這一章中的代碼不是完整的代碼,缺少 view 的頭文件,不過這只是一個(gè)空白的文件。你也可以直接把 view 的代碼放到 main()函數(shù)里面運(yùn)行。

本文出自 “豆子空間” 博客,請(qǐng)務(wù)必保留此出處 http://devbean.blog.51cto.com/448512/193918

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)