閱讀921 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Qt Mode/View 學習筆記 —— 概述和Model

介紹

Qt 4推出了一組新的item view類,它們使用model/view結構來管理數據與表示層的關係。這種結構帶來的功能上的分離給了開發人員更大的彈性來定製數據項的表示,它也提供一個標準的model接口,使得更多的數據源可以被這些item view使用。這裏對model/view的結構進行了描述,結構中的每個組件都進行了解釋,給出了一些例子說明了提供的這些類如何使用。

Model/View 結構

Model-View-Controller(MVC), 是從Smalltalk發展而來的一種設計模式,常被用於構建用戶界麵。經典設計模式的著作中有這樣的描述:

MVC由三種對象組成。Model是應用程序對象,View是它的屏幕表示,Controller定義了用戶界麵如何對用戶輸入進行響應。在MVC之前,用戶界麵設計傾向於三者結合在一起,MVC對它們進行了解耦,提高了靈活性與重用性。

假如把view與controller結合在一起,結果就是model/view結構。這個結構依然是把數據存儲與數據表示進行了分離,它與MVC都基於一樣的思想,但它更簡單一些。這種分離使得在幾個不同的view上顯示同一個數據成為可能,也可以重新實現新的view,而不必改變底層的數據結構。為了更靈活的對用戶輸入進行處理,引入了delegate這個概念。它的好處是,數據項的渲染與編程可以進行定製。


如上圖所示,model與數據源通訊,並提供接口給結構中的其他組件使用。通信的性質依賴於數據源的種類與model實現的方式。view從model獲取model indexes,後者是數據項的引用。通過把model indexes提供給model,view可以從數據源中獲取數據。在標準的views中,delegate會對數據項進行渲染,當某個數據項被選中時,delegate通過model indexes與model直接進行交流。

總的來說,model/view相關類可以被分成上麵所提到的三組:models、views、delegates。這些組件通過抽象類來定義,它們提供了共同的接口,在某些情況下,還提供了缺省的實現。抽象類意味著需要子類化以提供完整的其他組件希望的功能。這也允許實現定製的組件。

Models、views、delegates之間通過信號—槽機製來進行通信:

從model發出的信號通知view數據源中的數據發生了改變。

從view發出的信號提供了有關被顯示的數據項與用戶交互的信息。

從delegate發生的信號被用於在編輯時通知model和view關於當前編輯器的狀態信息。

Models(模型)

所有的item models都基於QAbstractItemModel類,這個類定義了用於views和delegates訪問數據的接口。數據本身不必存儲在model,數據可被置於一個數據結構或另外的類、文件、數據庫、或其它的程序組件中。

QAbstractItemModel提供給數據一個接口,它非常靈活,基本滿足views的需要,無論數據用以下任何形式表現,如table、list、trees。然而,當你重新實現一個model時,如果它基於table或list形式的數據結構,最好從QAbstractListModel、QAbstractTableModel開始做起,因為它們提供了適當的常規功能的缺省實現。這些類可以被子類化以支持特殊的定製需求。

Qt提供了一些現成的models用於處理數據項:

QStringListModel 用於存儲簡單的QString項目列表。

QStandardItemModel管理更複雜的樹型結構數據項,每項都可以包含任意數據。

QFileSystemModel提供本地文件係統中的文件與目錄信息。

QSqlQueryModel、QSqlTableModel、QSqlRelationTableModel用來訪問數據庫。

如果這些標準models不能滿足需要,可以使用子類化QAbstractItemModel、QAbstractListModel或是QAbstractTableModel來定製models。

Views(視圖)

不同的view都完整實現了各自的功能:

QListView把數據顯示為一個列表,QTableView把Model 中的數據以table的形式表現,QTreeView用具有層次結構列表來顯示model中的數據。這些類都基於QAbstractItemView抽象基類。盡管這些類都已經完整地得到了實現,但它們仍然可以用於子類化以滿足定製需求。

Delegates(委托)

QAbstractItemDelegate是model/view架構中的用於delegate的抽象基類。缺省的delegate實現在QStyledItemDelegate類中提供,它可以用於Qt標準views的缺省 delegate。但是,QStyledItemDelegateQItemDelegate是相互獨立的用於實現視圖(views)中items的描繪和編輯功能的方法。它們兩者的不同在於,QStyledItemDelegate使用當前的樣式(style)來描繪items。因此,當我們實現定製委托(delegate)或者使用Qt Style Sheets時,我們建議使用QStyledItemDelegate作為基類。

Sorting(排序)

在model/view架構中,有兩種方法進行排序,選擇哪種方法依賴於你的底層Model。

假如你的model是可排序的,也就是它重新實現了QAbstractItemModel::sort()函數,QTableView與QTreeView都提供了API,允許你以編程的方式對model數據進行排序。另外,你也可以進行交互方式下的排序(例如,允許用戶通過點擊view表頭的方式對數據進行排序),具體方法是:把QHeaderView::sectionClicked()信號與QTableView::sortByColum()槽或QTreeView::sortByColumn()槽進行關聯。

另一種方法是,假如你的model沒有提供需要的接口或是你想用list view表示數據,可以用一個代理model在用view表示數據之前對你的model數據結構進行轉換。

Convenience classes(便利類)

許多便利類都源於標準的view類,它們方便了那些使用Qt中基於項的view與table類,它們不應該被子類化,它們隻是為Qt 3的等價類提供一個熟悉的接口。這些類有QListWidget、QTreeWidget、QTableWidget,它們提供了如Qt 3中的QListBox、 QlistView、QTable相似的行為。這些類比View類缺少靈活性,不能用於任意的models,推薦使用model/view的方法處理數據。

Using models and views(使用模型和視圖)

Qt提供了兩個標準的models:QStandardItemModelQFileSystemModel

QStandardItemModel是一個多用途的model,可用於表示list,table,tree views所需要的各種不同的數據結構。這個model也持有數據。

QFileSystemModel維護相關的目錄內容的信息,它本身不持有數據,僅是對本地文件係統中的文件與目錄的描述。QFileSystemModel是一個現成的model,很容易進行配置以利用現有的數據。使用這個model,可以很好地展示如何給一個現成的view設定model,研究如何用model indexes來操縱數據。

model與views的搭配使用

QListView與QTreeView很適合與QFileSystemModel進行搭配。下麵的例子在tree view與list view顯示了相同的信息,QFileSystemModel提供了目錄內容數據。這兩個Views共享用戶選擇,因此每個被選擇的項在每個view中都會被高亮。

/*先創建QF<span >i</span>leSystemModel以供使用,再創建views去顯示目錄的內容。這裏展示了使用model的最簡單的方式。model的創建與使用都在main()函數中完成:*/
int main(int argc, char *argv[])
{
   QApplication app(argc, argv);
   QSplitter *splitter = new QSplitter;
   /*此模型model的創建是為了使用特定文件係統的數據。setRootPath()調用告訴驅動此文件係統的model將數據呈現給哪些視圖(Views)*/
   QFileSystemModel *model = new QFileSystemModel;
   model->setRootPath(QDir::currentPath());
   /*創建兩個視圖(Views),分別是TreeView和ListView*/
   /*配置一個View以顯示模型model中的數據,隻需簡單調用setModel()函數,並將目錄model作為參數。setRootIndex()告訴views顯示哪個目錄的信息,這需要提供一個model index,然後用這個model index去model中去獲取數據。index()函數是QFileSystemModel特有的,通過把一個目錄做為參數,得到了需要的model index。*/
   QTreeView *tree = new QTreeView(splitter);
   tree->setModel(model);
   tree->setRootIndex(model->index(QDir::currentPath()));
   QListView *list = new QListView(splitter);
   list->setModel(model);
   list->setRootIndex(model->index(QDir::currentPath()));
   /*將所有的widgets置於splitter中*/
   splitter->setWindowTitle("Two views onto the same file system model");
   splitter->show();
   return app.exec();
}
上麵的例子並沒有展示如何處理數據項的選擇,這包括很多細節,以後會提到。

Model類

基本概念

在model/view構架中,model為view和delegates使用數據提供了標準接口。在Qt中,標準接口QAbstractItemModel類中被定義。不管數據在底層以何種數據結構存儲,QAabstractItemModel的子類會以層次結構的形式來表示數據,結構中包含了數據項表。我們按這種約定來訪問model中的數據項,但這個約定不會對如何顯示這些數據有任何限製。數據發生改變時,model通過信號槽機製來通知關聯的views。


Model Indexes

為了使數據存儲與數據訪問分開,引入了model indexes的概念。通過model indexes,可以引用model中的數據項,views和delegates都使用indexes來訪問數據項,然後再顯示出來。因此,隻有model需要了解如何獲取數據,被model管理的數據類型可以非常廣泛地被定義。Model indexes包含一個指向創建它們的model的指針,這會在配合多個model工作時避免混亂。

QAbstractItemModel *model = index.model();
Model indexes提供了對一項數據信息的臨時引用,通過它可以訪問或是修改model中的數據。既然model可能不時重新組織內部的數據結構,model indexes可能會失效,因此不應該保存臨時的model indexes。假如需要一個對數據信息的長期的引用,那麼應該創建一個persistent model index。這個引用會保持更新。臨時的model indexes由QModelIndex提供,而具有持久能力的model indexes則由QPersistentModelIndex提供。

在獲取對應一個數據項的model index時,需要考慮有關於model的三個屬性:行數,列數,父項的model index。

行與列

在最基本的形式中,一個model可作為一個簡單的表來訪問,每個數據項由行數,列數來定位。這並不意味著底層的數據用數組結構來存儲。行和列的使用僅僅是一種約定,它允許組件之間相互通信。可以通過指定model中的行列數來獲取任一項數據,可以得到與數據項一一對應的那個index。

QModelIndex index = model->index(row, column, ...);
Model為簡單的、單級的數據結構如list與tables提供了接口,它們如上麵代碼所顯示的那樣,不再需要別的信息被提供。當我們在獲取一個model index時,我們需要提供另外的信息。


上圖代表一個基本的table model,它的每一項用一對行列數來定位。通過行數和列數,可以獲取代表一個數據項的model index :

QModelIndex indexA = model->index(0, 0,QModelIndex());
QModelIndex indexB = model->index(1, 1,QModelIndex());
QModelIndex indexC = model->index(2, 1,QModelIndex());
一個model的頂層項由QModelIndex()取得,它們被用作父項。

父項

類似於表的接口在搭配使用table或list view時是理想的,這種行列係統與view顯示的方式是確切匹配的。然而,像tree views這種結構需要model提供更為靈活的接口來訪問數據項。每個數據項可能是其它的項的父項,上級的項可以獲取下級項的列表。當獲取model中數據項的index時,我們必須指定關於數據項的父項的信息。在model外部,引用一個數據項的唯一方法就是通過model index,因此需要在求取model index時指定父項的信息。

QModelIndex index = model->index(row, column, parent);

上圖中,A項和C項作為model中頂層的兄弟項:

QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
A有許多孩子,它的一個孩子B用以下代碼獲取:

QModelIndex indexB = model->index(1, 0, indexA);

項角色

model中的項可以作為各種角色來使用,這允許為不同的環境提供不同的數據。舉例來說,Qt::DisplayRole被用於訪問一個字符串,它作為文本會在view中顯示。典型地,每個數據項都可以為許多不同的角色提供數據,標準的角色在Qt::ItemDataRole中定義。

我們可以通過指定model index與角色來獲取我們需要的數據:

QVariant value = model->data(index, role);

角色指出了從model中引用哪種類型的數據。views可以用不同的形式顯示角色,因此為每個角色提供正確的信息是非常重要的。通過為每個角色提供適當數據,model也為views和delegates提供了暗示,以指示如何正確地把這些數據項顯給用戶。不同的views可以自由地解析或忽略這些數據信息,對於特殊的應用要求,也可以定義一些附加的角色。

基本概念總結:

1、Model indexes為views與delegates提供model中數據項定位的信息,它與底層的數據結構無關。

2、通過指定行數、列數以及父項的model index來引用數據項。

3、依照其它的組件的要求,model indexes被model構建,如views和delegates。

4、使用index()時,如果指定了有效的父項的model index,那麼返回得到的model index對應於父項的某個孩子。

5、使用index()時,如果指定了無效的父項的model index,那麼返回得到的model index對應於頂層項的某個孩子。

6、角色對一個數據項包含的不同類型的數據給出了區分。

使用Model Indexes

為了演示數據是如何通過model indexes來從model中獲取數據,我們創建了一個沒有views的QFileSystemModel ,這個Model用於在一個widget中顯示文件名和目錄路徑。盡管這個例子並沒有展現出一個使用Model的正常方法,但是我們可以通過此例來了解處理Model indexes的一些約定和規範。

首先,我們創建一個文件係統Model。

QFileSystemModel *model = new QFileSystemModel;
QModelIndex parentIndex = model->index(QDir::currentPath());
/*計算Model中的行數*/
int numRows = model->rowCount(parentIndex);
/*這裏,為了簡約,我們僅僅隻關注Model每一列的第一項。我們依次檢查每一行,獲取該行第一項的Model index。然後在讀取該項的內容。*/
for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);
/*在這裏,理解data()函數的用法,data()函數用於獲取指定index處的數據,同時,通過Qt::DisplayRole來以String的方式獲取該項內容。*/
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
}
以上的例子說明了從model中獲取數據的基本原則:

1、Model的尺寸可以從rowCount()columnCount()中得出。這些函數通常都需要一個表示父項的model index。

2、Model indexes用來從model中訪問數據項,數據項用行數,列數以及父項來實現model index定位。

3、為了訪問model頂層項,可以使用QModelIndex()指定一個Null Model Index。

4、數據項為不同的角色提供不同的數據。為了獲取數據,除了model index之外,還要指定角色。

創建新的Models

model/view組件之間功能的分離,允許創建model利用現成的views。這也可以使用標準的功能圖形用戶接口組件像QListView、QTableView和QTreeView來顯示來自各種數據源的數據。

QAbstractListModel類提供了非常靈活的接口,允許數據源以層次結構的形式來管理信息,也允許以某種方式對數據進行插入、刪除、修改和排序。它也提供了對拖拽操作的支持。

QAbstractListModelQAbstractTableModel為簡單的非層次結構的數據提供了接口,對於比較簡單的list和table models來說,這是不錯的一個開始點。

設計一個Model

當我們為存在的數據結構新建一個model時,首先要考慮的問題是應該選用哪種model來為這些數據提供接口。

假如數據結構可以用數據項的列表或表格來表示,那麼可以考慮子類化QAbstractListModelQAbstractTableModel,因為這些類已經合理地對許多功能提供缺省實現。

然而,假如底層的數據結構隻能表示成具有層次結構的樹型結構,那麼必須得子類化QAbstractItemModel。

無論底層的數據結構采取何種形式,在特定的model中實現標準的QAbstractItemModel API總是一個不錯的主意,這使得可以使用更自然的方式對底層的數據結構進行訪問。這也使得用數據構建model更為容易,其他的model/view組件也可以使用標準的API與之進行交互。

一個隻讀model示例

這個示例實現了一個簡單的、非層次結構的、隻讀的數據model,它基於QStringListModel類。它有一個QStringList作為它內部的數據源,隻實現了一些必要的接口。為了簡單化,它子類化了QAbstractListModel,這個基類提供了合理的缺省行為,對外提供了比QAbstractItemModel更為簡單的接口。當我們實現一個model時,不要忘了QAbstractItemModel本身不存儲任何數據,它僅僅提供了給views訪問數據的接口。

class StringListModel : public QAbstractListModel
{
Q_OBJECT
public:
StringListModel(const QStringList &strings, QObject *parent = 0)
: QAbstractListModel(parent), stringList(strings) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
private:
QStringList stringList;
};

除了構造函數,我們僅需要實現兩個函數:rowCount()返回model中的行數,data()返回與特定model index對應的數據項。

具有良好行為的model也會實現headerData(),它返回treetable views需要的在標題中顯示的數據(標題欄)。

因為這是一個非層次結構的model,我們不必考慮父子關係。假如model具有層次結構,我們也應該實現index()parent()函數。

Model的尺寸

我們認為model中的行數與string list中的string數目一致:

int StringListModel::rowCount(const QModelIndex &parent) const
{
return stringList.count();
}
在缺省情況下,從QAbstractListModel派生的model隻具有一列,因此不需要實現columnCount()。

Model 標題與數據

QVariant StringListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();// QVariant is invalid data.
    if (index.row() >= stringList.size())
        return QVariant();
    if (role == Qt::DisplayRole)
        return stringList.at(index.row());
    else
        return QVariant();
}
對於一些視圖(Views),如QTreeView和QTabeleView,我們需要設置對應的數據項的標題。這裏,我們使用headerData()函數來實現添加行列標題的目的:

QVariant StringListModel::headerData(int section, Qt::Orientation orientation,
                                     int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();
    if (orientation == Qt::Horizontal)
        return QString("Column %1").arg(section);
    else
        return QString("Row %1").arg(section);
}

一個數據項可能有多個角色,根據角色的不同輸出不同的數據。上例中,model中的數據項隻有一個角色DisplayRole,然而我們也可以重用提供給DisplayRole的數據,作為別的角色使用,如我們可以作為ToolTipRole來用。

可編輯的model

上麵我們演示了一個隻讀的model,它隻用於向用戶顯示,對於許多程序來說,可編輯的list model可能更有用。我們隻需要給隻讀的model提供另外兩個函數flags()與setData()的實現。下列函數聲明被添加到類定義中:

    Qt::ItemFlags flags(const QModelIndex &index) const;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole);

使model可編輯

delegate會在創建編輯器之前檢查數據項是否是可編輯的。model必須得讓delegate知道它的數據項是可編輯的。這可以通過為每一個數據項返回一個正確的標記得到,在本例中,我們假設所有的數據項都是可編輯可選擇的:

Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::ItemIsEnabled;
    return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
}

我們不必知道delegate執行怎樣實際的編輯處理過程,我們隻需提供給delegate一個方法,delegate會使用它對model中的數據進行設置。這個特殊的函數就是setData():

bool StringListModel::setData(const QModelIndex &index,
                              const QVariant &value, int role)
{
    if (index.isValid() && role == Qt::EditRole) {
        stringList.replace(index.row(), value.toString());
        emit dataChanged(index, index);
        return true;
    }
    return false;
}

當數據被設置後,model必須得讓views知道一些數據發生了變化,這可通過發射一個dataChanged()信號實現。因為隻有一個數據項發生了變化,因此在信號中說明的變化範圍隻限於一個model index。

同時,data()函數也需要進行更改來添加Qt::Editable測試:

QVariant StringListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    if (index.row() >= stringList.size())
        return QVariant();
    if (role == Qt::DisplayRole || role == Qt::EditRole)
        return stringList.at(index.row());
    else
        return QVariant();
}

插入,刪除行

在model中改變行數與列數是可能的。當然在本列中,隻考慮行的情況,我們隻需要重新實現插入、刪除的函數就可以了,下麵應在類定義中聲明:

    bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex());
    bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex());

既然model中的每行對應於列表中的一個string,因此,insertRows()函數在string list 中指定位置插入一個空的string。

Parent和index通常用於決定model中行列的位置。本例中隻有一個單獨的頂級項,因此隻需要在list中插入空string。

bool StringListModel::insertRows(int position, int rows, const QModelIndex &parent)
{
    beginInsertRows(QModelIndex(), position, position+rows-1);
    for (int row = 0; row < rows; ++row) {
        stringList.insert(position, "");
    }
    endInsertRows();
    return true;
}

函數首先調用beginInsertRows()通知其他組件行數將會改變。然後確定插入的第一行和最後一行所對應的行號,以及父項的Model Index。在插入所有字符串表項後,調用endInseRows()來完成所有操作同時通知其他組件model尺寸的改變,返回TRUE來表示插入操作的成功。

刪除操作與插入操作類似:

bool StringListModel::removeRows(int position, int rows, const QModelIndex &parent){
    beginRemoveRows(QModelIndex(), position, position+rows-1);
    for (int row = 0; row < rows; ++row) {
        stringList.removeAt(position);
    }
    endRemoveRows();
    return true;
}

最後更新:2017-04-03 05:39:49

  上一篇:go 【Agile Pair Coding】Data Type Mapping
  下一篇:go 【算法小總結】母函數模板