Qt Model/View程式設計介紹
Qt中包含了一系列的項檢視類,它們使用model/view的架構去管理資料之間的關係以及它們被展示給使用者的方式。這種由這種架構引進的功能分離特性給了開發者很大的靈活性去自定義自己的展示方式,並且提供了一個編制的模型介面以使很多種的資料來源都可以和現存的檢視相結合。那麼,今天我們就來簡單的看下這種model/view範例,相關的概念,並簡要描述一下項檢視系統的架構。
模型/檢視 架構
Model-View-Controller(MVC)是一種起源於Smalltalk程式語言的設計模式,主要被用來構建使用者介面。對於這種設計模式,Gamma et al 寫道:
MVC由3中物件組成。模型(Model)是應用程式的資料,檢視(View)是它的螢幕展示,控制器(Controller)則定義了使用者介面響應使用者輸入的方式。在MVC之前,使用者介面的設 計傾向於將這些東西雜揉到一起。MVC則通過對它們進行解耦來提高開發的靈活性和元件的複用性。
如果檢視和控制器被組合在了一起,結果就是model/view架構了。這仍然區分了資料的儲存方式和它被展示的方式,不過是基於相同的原則提供了一個更簡單的框架。這種分離使我們有可能用多種不同的檢視來展示同一種資料,並且可以在不改變底層資料結構的情況下實現新的檢視型別。而為了靈活的處理使用者輸入,我們又引入了代理(Delegate)的概念。在model/view框架中引入代理的好處是允許資料項的渲染和編輯可以被自定義。所以,傳統的MVC在Qt中就變成了MVD,其工作原理圖如下:
其中,模型和資料來源進行互動,為架構中的其他元件提供一個介面。當然,互動的本質取決於資料來源的型別和模型被實現的方式;檢視從模型中獲得模型下標(model indexes),這些下標是對資料項的引用。通過為模型應用模型下標,檢視就可以從資料來源中獲得資料項。在標誌檢視下,會有一個代理來渲染這些資料項。當資料項被編輯時,代理又會使用模型下標直接和模型進行互動。
通常情況下,model/view相關的類可以分為三組:模型,檢視和代理。這些元件中的每一個都有相關的抽象類來定義,以此來提供一些通用的介面,並在某些情況下,提供一些特性的預設實現。這些抽象類可以被子類化以為其他元件提供完全的功能支援,也可以藉此實現一些特定的元件。
模型,檢視和代理彼此使用訊號和槽進行通訊:
- 來自模型的訊號會通知檢視關於資料來源中資料的改變
- 來自檢視的訊號提供了使用者和檢視項發生互動的資訊
- 來自代理的訊號被用於在編輯過程中告知模型和檢視當前編輯器的狀態。
- QStringListModel:被用來儲存一個QString項的簡單列表。
- QStanderItemModel:可以用來管理更復雜的樹型資料結構,每一個數據項可以包含任意資料。
- QFileSystemModel:提供本地檔案系統中檔案和目錄的資訊。
- QSqlQueryModel,QSqlTabelModel和QSqlRelationalTableModel 被用作訪問資料庫的方便方法。
其實現程式碼如下:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSplitter *splitter = new QSplitter;
QFileSystemModel *model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());
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()));
splitter->setWindowTitle("Two views onto the same file system model");
splitter->show();
return app.exec();
}
我們例項化了一個QFileSystemModel以便使用,並建立了相應的檢視來展示一個目錄下的內容。這是使用模型的簡單的方式。該模型被初始化為使用一個檔案系統的資料。呼叫setRootPath()告訴模型要將檔案系統上的哪一個驅動器展示到檢視中。我們在此建立了兩個檢視以便於用兩種方式測試儲存在模型中的資料。
由上面的程式碼可以看出,檢視的建立方式和其他控制元件一樣。要設定一個檢視要顯示的模型只需用要使用的模型作為引數呼叫檢視類的setModel()。我們使用每一個檢視的setRootIndex()來過濾模型提供的資料,在此我們為該函式傳入了一個代表當前目錄的的模型下標。上面所使用index()函式對應QFileSystemModel來說是唯一的;我們為它傳入一個目錄,它為我們返回一個模型下標。
至於這麼處理檢視中項的選擇操作,我們會在後面的部分講解。
模型類
在處理選擇操作之前,我們先來看下模型/檢視中的一些概念。
基本概念
在模型/檢視架構中,模型為檢視和代理提供了訪問資料的標準介面。在Qt中,這些標準介面在QAbstractItemModel類中被定義。無論資料項儲存在什麼資料結構中,所有的QAbstractItemModel的子類都會把資料渲染為一個包含專案表的層級結構。檢視使用這個約定的方式訪問模型中的資料項,但它們也可以使用其他的方式來訪問資料,並不侷限於用和它們向用戶展示資料的相同的方式。
同時,模型還會使用訊號和槽向關聯的檢視通知資料的改變。 模型下標 為了確保資料的展示和資料的訪問分開,便引入了模型下標的概念。通過模型可以獲得的任何資訊都是由一個模型下標表示的。檢視和代理使用這些下標去請求要顯示的資料項。 這樣一來,只有模型需要知道怎麼獲取資料,並且被模型管理的資料的型別可以被定義的非常廣泛。模型下標中包含一個指向建立它們的模型的指標,這在使用多個模型時可以避免混淆。
QAbstractItemModel *model = index.model();
模型下標提供了對資訊片段的臨時引用,可以被用來通過模型去獲取或修改資料。因為模型隨時都有可能重新組織它們的內部結構,模型下標可能會失效,所以不應該儲存它們到一個變數中。如果需要對某種資訊的長期引用,必須建立一個永續性的模型下標。這種下標會提供一個對模型資訊的引用,並保持更新。臨時模型下標由QModelIndex類表示,永續性的模型下標由QPersistentModelIndex類表示。
要得到一個對應於某個資料項的模型下標,則必須指明三個屬性:一個行號,一個列號,一個該資料項父項的模型下標。下面,我們就來看一看這些屬性。
行和列
在其最基本的形式下,一個模型可以被當做一個簡單的表格來訪問,此時每一個數據項都可以被它們所在的行和列來確定。但這並不意味著底層的資料是儲存在一個數組結構中的;所使用的行號和列號只是一個約定,執行元件間彼此進行通訊。我們可以通過指定一個數據項在模型中的行號和列號來得到任何資料項的資訊,模型會向我們返回一個該資料項所對應的模型下標:
QModelIndex index = model->index(row, column, ...);
對於那些為簡單、單級資料結構如列表和表格提供介面的模型來說,在訪問一個數據項時,除了行號和列號,不需要提供其他的資訊。但是,像上面的程式碼顯示的,我們需要提供其他的資訊才能獲得一個模型下標。上圖表示了一個基本的表格模型,其中的每一項由一個行和列來確定。我們通過傳遞相對於該模型的行和列來得到一個引用某個資料項的模型下標。
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
對應模型中的頂層項來說,我們總是傳入一個QModelIndex()來作為它們的父項。我們會在後面的部分繼續討論這個問題。
父元素項
當在一個表格或列表中使用資料時,類表格介面對於模型提供的資料來說是完美的;行列號系統精確的對映於檢視顯示資料的方式上。但是,樹型檢視這類的結構需要模型對其中的資料暴露更靈活的介面。結果,每一個項都可能是另一個表格專案的父,進而在樹型檢視中一個頂層專案可能包含另一個專案列表。
當請求一個模型項的下標時,我們必須提供該項的父項的相關資訊。因為,在模型之外,引用一個項的唯一方式是通過一個模型下標,所以必須傳入一個模型下標:
QModelIndex index = model->index(row, column, parent);
上圖展示了一個樹型檢視,其中的每一項都由一個父項,一個行號,一個列號來確定。 項 "A" 和 "C" 是頂層兄弟項,可以用下面的方式獲得:
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
項"A"有一系列的孩子。其中,"B"的模型下標可以用下面的方式獲得:
QModelIndex indexB = model->index(1, 0, indexA);
項的角色
在模型中的每一項對其他元件來說都可能扮演多種角色,以允許不同種類的資料被應用於不同的情況。例如,Qt::DisplayRole是用來訪問一個可以在檢視中按文字顯示的字串。典型的,資料項包含了不同角色的資料,標準的角色由Qt::ItemDataRole型別來定義。
我們可以向模型請求某個項的資料,通過向它傳遞該項的模型下標,以及我們想要的資料型別的角色:
QVariant value = model->data(index, role);
對於大部分常見用途的項資料,Qt::ItemDataRole型別的定義中均已包括。通過為每一種角色提供合適的項資料,模型可以為檢視和代理提供提示該如果向用戶展示該項。不同種類的檢視均可以接受或忽略這些需要的資訊。當然,也可以為特定的應用程式目的定義特定的角色。自此,對於模型類的使用,我們可以進行一下總結:
- 模型下標以獨立於底層資料結構的方式向檢視和代理提供了模型中的資料項的資訊。
- 項是由它們在模型中的行號和列號,以及它們的父項所指定的。
- 模型下標在其他元件,如檢視和代理,發出請求時被模型創建出來。
- 當我們使用index() 函式請求一個下標時,若傳遞了一個有效的模型下標做為父項,那麼返回的下標引用了一個在模型中位於父項下面的某項。這個下標引用著那個項的一個孩子。
- 若使用index() 函式時,傳入了一個無效的模型下標做為父項,那麼返回的下標引用著模型中的一個頂層項
- 角色區分了一個項所關聯的不同資料。
QFileSystemModel *model = new QFileSystemModel;
QModelIndex parentIndex = model->index(QDir::currentPath());
int numRows = model->rowCount(parentIndex);
如上面的程式碼所示,我們建立了一個預設的QFileSystemModel,使用模型類的函式index()獲取一個父下標,然後,我們使用模型類的rowCount()函式獲取了該下標中的行數。
出於簡單起見,我們只關心模型中第一列的資料。我們遍歷每一行,獲得每一行中第一項的模型下標,並讀取出那一項所儲存的資料。 for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
}
為了得到一個模型下標,我們指定了一個行號,一個列號(0代表第一個列),和一個代表我們想獲得的所有資料的父項的模型下標。而每一項中儲存的文字可以使用模型的data()函式獲取到。我們通過指定一個模型下標和相應的DisplayRole已字串的形式來獲得某項儲存的資料。
上面的例子雖小,但說明了從一個模型中獲取資料的基本原則:
- 一個模型的維度可以由rowCount() 和 columnCount()獲得。這些函式通常需要指定一個父項的模型下標。
- 模型下標被用來訪問模型中的項。行號、列號和父項的模型下標是必須的,對於定位一個模型項來說。
- 要訪問模型中的頂層專案,可以使用QModelIndex()指定一個空的模型下標作為父項。
- 項可以為不同的角色儲存資料。為了獲得特定角色的資料,需同時指定模型下標和角色。
上圖顯示的是標準檢視的預設行為,這已經足夠大部分應用程式使用的了。它們提供了基本的編輯裝置,並且可以被自定義去適應更特化的使用者介面的需求。 使用模型
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QStringList numbers;
numbers << "One" << "Two" << "Three" << "Four" << "Five";
QAbstractItemModel *model = new StringListModel(numbers);
QListView *view = new QListView;
view->setModel(model);
view->show();
return app.exec();
}
如上面程式碼所示,我們先建立了一個字串列表模型(自定義),再為它初始化一些資料,然後建立一個檢視顯示這個模型的內容。
注意,StringListModel被宣告為QAbstractItemModel。這允許我們對這個模型應用抽象的介面,並可以確保當我們用一個不同的模型來替換字串列表模型時,我們的程式碼任然能夠工作。
由QListView提供的列表檢視足夠展示這個字串列表了。該檢視會渲染這個模型中的內容,通過這個模型的介面訪問資料。當用戶嘗試去編輯一個項時,該檢視會使用預設的代理會使用者提供一個編輯控制元件。上圖說明了QListView是如何展示這個字串列表模型的。另外,因為該模型時可編輯的,所以,檢視自動的使用預設代理使用列表中的每一項都可編輯。 為多個檢視使用一個模型 在同一個模型上應用多個檢視是很簡單的,只需在每個檢視上呼叫setModel()即可。程式碼如下:
QTableView *firstTableView = new QTableView;
QTableView *secondTableView = new QTableView;
firstTableView->setModel(model);
secondTableView->setModel(model);
在模型/檢視中,訊號和槽的使用意味著模型的改變可以被傳播到所有相關的檢視上,以此來確保無論使用哪個檢視,我們總能訪問到相同的資料。上圖說明了兩個不的視如果作用在同一個模型上,並且,每一都包含了一些了選中的項。雖然,兩個檢視的資料是來自同一個模型,但每一個檢視都包含了它自己的內部選擇模型。這在很多情況下都是有用的。 處理元素的選中 檢視處理選中的機制是由QItemSelectionModel類提供的。所有標準的檢視在預設情況下都會建立它們自己的選中模型,並與它們進行正常的互動。我們可以使用selectionModel()函式獲得一個檢視當前正在使用的選中模型,也可以使用setSelectionModel()為檢視重新設定一個選中模型。當我們想要在同一個模型上提供多個一致的檢視時,這種控制檢視使用的選中模型的能力是很有用的。 通常情況下,除非你正在子類化一個模型或檢視,否則你不必直接操作selections的內容。但是,如果需要的話,我們可以訪問選中模型的介面。 在檢視間共享選中模型 經過預設情況下,檢視提供它們自己的選中模型對我們來說是方便的,但當我們在一個模型上使用多個檢視時,經常希望模型的資料和使用者的選中在所有的檢視中是一樣的。既然檢視類允許它們內部的選中模型被修改,我們就可以使用下面這行程式碼來在兩個檢視間共享選中模型:
secondTableView->setSelectionModel(firstTableView->selectionModel());
第二個檢視的選中模型被設定為第一個檢視的選中模型。此時,兩個檢視就工作與同一個選中模型了。它們會在資料和選擇操作上儲存一致。效果如下:
在上面的例子中,我們使用了兩個同樣型別的檢視和同一個模型資料。但是,如果使用兩種不同型別的檢視,那麼這些選中的項在兩個檢視將會得到不同的展現;例如,在表格檢視中一系列連續的選中項,在一個樹型檢視可能被展示為了一系列的高亮選項的片段。 代理類 概念 不像Model-View-Controller模式,model/view模式不包含一個完全獨立的元件來用於和使用者進行互動。通常情況下,由檢視負責模型資料的展示和處理使用者的輸入。為了使這種處理使用者輸入的方式更靈活,這些互動就由代理來完成。這些元件提供了輸入的能力,同時也負責在檢視中渲染各自的模型項。控制代理的標準介面在QAbstractiItemDelegate類中定義。 代理將可以自己渲染它們的內容通過實現paint()函式和sizeHint()函式。但是,簡單的基於控制元件的代理可以繼承QItemDelegate而不是QAbstarctItemDelegate類來實現,這可以享受到這些函式的預設實現。 代理所使用的編輯器可以通過讓控制元件去掌管編輯過程來實現,也可以由代理直接響應事件來實現。 使用現存的代理 Qt提供的標準檢視使用QItemDelegate類的例項來提供編輯裝置。代理介面的這種預設實現會以一種常規的樣式來渲染檢視中每一個模型項。包括:QListView,QTableView和QTreeView。 所有標準的角色都由標準檢視使用的預設代理來處理。每一個檢視使用的代理可以由itemDelegate()函式返回。而setItemDelegate()函式為標準檢視安裝一個自定義的代理,並且,為自定義的檢視設定代理也需要呼叫這個函式。 實現一個簡單的代理 下面我們使用QSpinBox來使用一個簡單的代理,主要用於顯示整數的模型上。雖然我們建立的是基於整數的表格模型,但我們可以很容易的使用QStandardModel來替換,因為自定義的代理控制著資料的入口。我們建立一個表格檢視來展示模型的內容,並使用自定義的代理來提供編輯功能。效果如下圖:
此處,我們選擇從QItemDelegate派生,因為我們不想去寫自定義的顯示函式。但是,我們仍然必須提供相應的函式來管理我們的控制元件。類宣告如下:
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = 0);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const Q_DECL_OVERRIDE;
void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const Q_DECL_OVERRIDE;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE;
};
注意,當代理被建立時,並不建立相關的編輯控制元件。我們只在需要時才建立一個編輯控制元件。
建立編輯器
在這個例子中,當表格檢視需要一個編輯器時,它會請求代理為正在被修改的項建立一個合適的編輯控制元件。createEditor()函式會提供代理建立合適控制元件所需要的一切:
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &/* option */,
const QModelIndex &/* index */) const
{
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false);
editor->setMinimum(0);
editor->setMaximum(100);
return editor;
}
注意,我們不需要儲存執行該控制元件的指標,因為檢視會負責在不需要它時將其釋放。我們在編輯器上安裝代理預設的事件過濾器,類確保它提供使用者期望的標準的編輯快捷鍵。也可以向編輯器新增額外的快捷鍵來提供更復雜的行為。 檢視會負責呼叫我們定義的函式來正確的設定編輯器的資料和尺寸。我們可以基於檢視提供的不同的下標創建出不同的編輯器。例如,如果我們有一列是整數,還一列是字串,那麼我們可以建立一個QSpinBox或者一個QLineEdit,取決於當前編輯的是哪一列。 代理同時必須提供一個函式將模型中的資料拷貝到編輯器。在這個例子中,我們讀取模型中display role的資料,並相應地將它設定到spinbox。程式碼如下:
void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
int value = index.model()->data(index, Qt::EditRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(value);
}
在這個例子中,我們知道編輯器控制元件是一個spix box,但是我們可能為模型中不同的資料型別提供了不同的代理,所以,我們需要先將控制元件轉化成正確的型別才能訪問它的成員函式。
提交資料到模型
當用戶在spin box中編輯結束,檢視會要請求代理將編輯後的值儲存到模型中,通過呼叫setModelData()函式來實現。程式碼如下:
void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const
{
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->interpretText();
int value = spinBox->value();
model->setData(index, value, Qt::EditRole);
}
既然是檢視幫代理管理著編輯器控制元件,所以我們只需要使用編輯器提供的值去更新模型即可。在這種情況下,我們要確保spin box 是最新的,並使用它包含的值通過指定的下標來更新模型。 標準的QItemDelegate類會在編輯結束後通過傳送closeEditor()訊號的方式來通知檢視。檢視就會確保編輯器控制元件被關閉和銷燬。但在這個例子中,我們只提供了簡單的編輯功能,所以我們不需要傳送這個訊號。 在資料上的所有操作都是通過QAbstractItemModel類的介面來實現的。這使代理在很大程度上獨立於其操作的資料型別,但為了使用特定型別的編輯器控制元件還是要做一個假定的。在這個例子中,我們假定模型總是包含整形資料,但我們仍然能將該代理用於不同型別的模型上,因為QVariant類可以為未知的資料提供合理的預設值。 更新編輯器的尺寸 管理編輯器的尺寸就是代理的責任了。當編輯器被建立時必須設定它的尺寸,當編輯器的尺寸或位置發生變化時也是如此。幸運的是,檢視在一個view option物件中提供了所有必須的尺寸資訊。程式碼如下:
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
editor->setGeometry(option.rect);
}
在這個例子中,我們僅僅使用了view option 物件在item rectangle 中提供的尺寸資訊。使用多種元素渲染資料項的代理可能不直接使用item rectangle。它可能會相對於該資料項中其他的元素來安置這個編輯器。編輯提示 編輯結束之後,代理應該向其他元件提供一些關於編輯結果的示意,並且還要為協助後續的編輯操作提供一些示意。這些操作可以通過傳送closeEditor()訊號,並攜帶一個合適的提示引數來完成。這會被我們在構造spin box時為其安裝的預設事件處理器來處理。 spin box的行為可以被調整的對使用者來說更加友好。在QItemDelegate提供的預設事件處理器中,如果使用者在spin box中敲擊了回車鍵來確定他們的選擇,代理物件就會將選擇的值提交到模型並關閉spin box。我們可以通過在spin box上安裝我們自己的事件過濾器,提供我們需要的合適的編輯提示,例如,我們可以在傳送closeEditor訊號時攜帶一個EditNextItem 提示,來實現自動編輯檢視中的下一項的功能。 另一個不需要使用事件過濾器的方法是提供我們自己的編輯控制元件,比如子類化QSpinBox。這個可選的方法相對於所編寫的額外程式碼來說,會使我們對編輯控制元件的行為有更多的控制。如果你需要自定義標準Qt編輯控制元件的行為的話,在代理上安裝一個事件過濾器通常也是很容易完成的。 當然,代理也不是必須要發出這些提示,只是說發出這些提示來支援通用的編輯動作的代理會更容易整合到應用程式中,更具有可用性。 處理檢視中的選擇操作 概念 檢視類所使用的選擇模型為基於模型/檢視架構的裝置的選擇提供了一個通用的描述。儘管檢視提供的標準類對操作選擇動作來說是足夠的,但選擇模型能允許你建立特定的選擇模型來滿足你自己的特定的模型檢視的需求。 檢視中被選中項的資訊儲存在一個QItemSelectionModel類的例項中。這個例項包含了這些項在一個獨立模型中的模型下標,並且獨立於任何檢視。因為一個模型可能會對映到多個檢視,所以我們可以在多個檢視間共享這些選擇,使應用程式以一種一致的方式來展示多個檢視。 選集是由選中範圍組成的。這使它可以通過僅僅重排每一個選中範圍的開始下標和結束下標就能高效的管理有大量選中項的選集的相關資訊。非連續性的選集會通過多個選中範圍來描述。 選集是被應用了一個選擇模型中的模型下標的結果。最近被應用的選中項被稱為當前選中項。該選中項的效果甚至可以在應用之後被修改,通過使用特定的選中命令。 當前項和選中項 在一個檢視中,總有一個當前項,一個選中項,兩種獨立的狀態。一個模型項同時可以既是當前項也是選中項。檢視有責任確保總有一個當前項作為鍵盤導航。下表列出了當前項和選中項的區別:
當前項 | 選中項 |
同一時間只有一個當前項 | 同一時間可以有多個選中項 |
當前項會隨著鍵盤導航或滑鼠點選而改變 | 每一項的選中狀態的設定或取消由幾個預定義的模式決定, 比如,單選模式,多選模式,等等。 |
當前項會在按下編輯鍵F2或滑鼠雙擊時被編輯 | 當前項可以和一個錨點一起使用來指定一個應該被選中或取消選中的範圍 (或者是兩個範圍的組合) |
當前項通過當前的矩形框來表明 | 選中項通過選中矩形來表明 |
當操作選集時,我們可以把QItemSelectionModel看作是一個模型中所有具有選中狀態的一個記錄集。一旦選擇模型被初始化,模型項就可以被選中,取消選中,或者在不需要做的哪一個模型項被選中的情況下翻轉它們的選中狀態。可以在任何時候獲得所有選中項的下標,並且可以通過訊號和槽機制來通知其他元件有關於選擇模型的改變。 使用選擇模型 標準檢視類提供的預設選擇模型可以在大部分應用程式中使用。可以通過檢視的selectionModel()方法獲得屬於一個檢視的選擇模型,也可以通過setSelectionModel()方法在多個檢視間共享它們,所以,通常情況下,不需要建立新的選擇模型。 我們可以通過為QItemSelection指定一個模型和一對模型下標來建立一個選集。選集使用下標去引用給定模型中的模型項,並把它們看作一個選中塊的左上角和右下角下標。要把這個選集應用到一個模型,需要先把這個選集提交到一個選擇模型上;我們有多種方式來完成這個操作,而每一種操作都對選擇模型中已有的項有不同的影響。 選擇模型項 為了說明選集的一些主要特性,我們用32個模型項建立了一個自定義表格模型的例項,並用一個表格檢視來展示它:
TableModel *model = new TableModel(8, 4, &app);
QTableView *table = new QTableView(0);
table->setModel(model);
QItemSelectionModel *selectionModel = table->selectionModel();
我們獲得了表格檢視的預設選擇模型以備後續使用。我們不修改模型中的任何項,只是選擇表格檢視中左上角的一些項。為實現這個功能,我們需要獲得將要被選中的區域的左上角和右下角模型下標:
QModelIndex topLeft = model->index(0, 0, QModelIndex());
QModelIndex bottomRight = model->index(5, 2, QModelIndex());
為了在模型中選中這些項,並且在表格檢視中看到相應的變化,我們需要構建一個選擇物件,在把它應用到選擇模型上:
QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);
該選集被應用到選擇模型,通過一個預定義的選集標誌。在上面這種情況下,所使用的的標誌導致我們所傳入的選集物件中的模型項被包含進了選擇模型,無論他們先前是什麼狀態。結果顯示如下圖:
這些項的選集可以使用多種預定義好的選集標誌進行修改。而這些操作所產生的選集的可能會有一個複合結構,但它仍可以被選中模型高效的展示。
讀取選集狀態
儲存在選擇模型中的模型下標可以使用selectedIndexed()方法讀取。返回的是一個無序的列表,其中包含了我們可以迭代的模型下標,只要我們知道它們屬於哪個模型:
QModelIndexList indexes = selectionModel->selectedIndexes();
QModelIndex index;
foreach(index, indexes) {
QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
model->setData(index, text);
}
選擇模型也是通過發射訊號來表現選擇的變化。這可以通知其他元件有關於模型中整個選集和當前有焦點的項的變化。我們可以連線selectionChanged()訊號到一個槽函式上,來測試當選集發 發生變化時模型中的項是被選中了還是被取消選中了。這個槽函式會接收兩個QItemSelection物件:一個包含新選中項的模型下標的列表;另一個包含的是相應的被取消選中的模型項的下標。
下面的程式碼中,我們為selectionChanged()訊號提供了一個槽函式,來為選中的項填充一個字串,並清空取消選中的項。
void MainWindow::updateSelection(const QItemSelection &selected, const QItemSelection &deselected)
{
QModelIndex index;
QModelIndexList items = selected.indexes();
foreach (index, items) {
QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
model->setData(index, text);
}
items = deselected.indexes();
foreach (index, items)
model->setData(index, "");
}
我們可以通過連線currentChanged()訊號到一個槽函式來跟蹤當前焦點項的改變,這個槽函式接受兩個模型下標作為引數,分別對應著前一個焦點項和當前焦點項。
下面的程式碼中,我們連線到currentChanged()訊號,然後使用接收到的資訊更新主視窗的狀態列:
void MainWindow::changeCurrent(const QModelIndex &t, const QModelIndex &previous)
{
statusBar()->showMessage(
tr("Moved from (%1,%2) to (%3,%4)")
.arg(previous.row()).arg(previous.column())
.arg(current.row()).arg(current.column()));
}
更新選集
選擇命令通過選擇標誌的組合來提供,選項標誌由QItemSelectionModel::SelectionFlag定義。每一個選擇標誌都告訴選擇模型當任何一個select()函式被呼叫時,該怎麼更新其內部的選中項的記錄集。最常使用的標誌是Select標誌,它指導選擇模型去把指定的模型項記錄為已選中。Toggle標誌使選擇模型去翻轉指定項的狀態,選中任何給定的未選中項,取消選中當前已選中的項。Deselect標誌取消選中所有指定的項。
選擇模型中個別的模型項也可以通過建立一個選集來更改,並把它們應用到一個選擇模型上。下面的程式碼,我們為上面的表格模型應用第二個選集,使用Toggle標誌來翻轉給定項的選中狀態:
QItemSelection toggleSelection;
topLeft = model->index(2, 1, QModelIndex());
bottomRight = model->index(7, 3, QModelIndex());
toggleSelection.select(topLeft, bottomRight);
selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);
執行結果如下:
預設情況下,選擇命令僅僅作用於由模型下標指定的個別項上。但是,描述選擇命令的標誌可以和其他的標誌進行組合,以此來改變整行和整列。例如,如果你只使用一個下標來呼叫select()函式,但傳入的選擇命令是Select和Rows的組合,那麼包含該行的整行都被選中。下面的程式碼展示了Rows和Columns標誌的使用:
QItemSelection columnSelection;
topLeft = model->index(0, 1, QModelIndex());
bottomRight = model->index(0, 2, QModelIndex());
columnSelection.select(topLeft, bottomRight);
selectionModel->select(columnSelection,
QItemSelectionModel::Select | QItemSelectionModel::Columns);
QItemSelection rowSelection;
topLeft = model->index(0, 0, QModelIndex());
bottomRight = model->index(1, 0, QModelIndex());
rowSelection.select(topLeft, bottomRight);
selectionModel->select(rowSelection,
QItemSelectionModel::Select | QItemSelectionModel::Rows);
雖然只向選擇模型提供了4個下標,但使用了Columns和Rows標誌,意味著2行和2列被選中。執行結果如下:
在上面例子模型中展示的命令都涉及到了在模型中累加一個選集中的模型項。當然,我們也可以清空一個選集,或用一個新的選集來替代當前的選集。要用一個新的選集替換當前的選集,可以在選擇標誌中組合Current標誌。該標誌就是告訴選擇模型去用呼叫select()函式傳入的模型下標替換它當前選集中的模型下標。要清空一個選集,可以在選擇標誌中組合Clear標誌,這會重置選擇模型中的模型下標的集合。
選擇模型中的所有項
為了選中模型中的所有項,我們需要為每一個層級建立一個能覆蓋該層級中所有項的選集。我們通過獲得左上角和右下角的項的模型下標來完成這件事:
QModelIndex topLeft = model->index(0, 0, parent);
QModelIndex bottomRight = model->index(model->rowCount(parent)-1,
model->columnCount(parent)-1, parent);
然後,我們用這些下標來建立一個選集,使在該範圍中的模型項都被選中:
QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);
這需要應用於模型中的所有層級。對於頂層項來說,我們可以使用下面的方式為它們定義一個父下標:
QModelIndex parent = QModelIndex();
對於有層級的模型,函式hasChildren()可以用來測試一個給定的項是否是另一個層級的父。
自定義模型
模型/檢視中功能的分離允許我們建立自定義的模型,同時還可以利用已存在的檢視。這中方法可以讓我們使用標準的圖形使用者介面元件,如QListView,QTableView和QTreeView,來展示來自不同資料來源的資料。
QAbstractItemModel類提供了一個足夠靈活的介面來支援一層級的方式安排資訊的資料來源,允許資料以某種方式被插入,刪除,修改或排序。它還支援拖放操作。
QAbstractListModel和QAbstractTableModel類為更簡單的非層級資料結構提供了介面支援,對於簡單的列表和表格模型這也是一個更容易使用的開始點。
下面,我們先建立一個簡單的只讀模型來展示基本的模型/檢視架構的慣例。在後面,我們會改變這個簡單模型,使它的項可以被使用者修改。
設計一個模型
當為現存的資料結構建立一個新模型時,考慮哪種模型應該用來為資料提供介面是很重要的。如果該資料結構可以被展示為一個列表或表格,那麼可以子類化QAbstractListModel 或者 QAbstractTableModel,因為這些類為很多操作提供了預設實現。
但是,如果底層的資料結構只能被展示為一個層級的樹形結構,那就必須子類化QAbstractItemModel。
在這裡,我們基於字串列表實現一個簡單的模型,所以QAbstractListModel是很好的一個基類選擇。
其實,無論底層的資料結構是什麼樣的,在一個特化的模型中,為了更自然的訪問底層資料結構,補充實現一些標準的QAbstractItemModel API總是一個好主意。這會讓模型的填充更容易,也能使其他常規的模型/檢視元件使用標準的API與它互動。下面自定義的模型出於目的提供了一個建構函式。
一個只讀的模型
這兒實現的模型是一個簡單的,非層級的,只讀的,基於QStringListModel的模型。它有一個QStringList作為它的內部資料結構,只實現了能讓模型執行的必須的函式。為了使實現更簡單,我們子類化QAbstractListModel。當實現一個模型時,要注意的是QAbstractItemModel並不儲存資料本身,它僅僅提供一個檢視訪問資料的介面。對於一個最小化的只讀模型來說,我們只需要實現很少的幾個函式,因為大部分介面都有了預設的實現。該類的宣告如下:
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()返回模型中資料的行數,data()返回模型中相對於特定模型下標的項。
行為良好的模型,還應該實現headerData()函式,來為樹形或表格檢視提供一些顯示在頭部的資訊。
注意,這是一個非層級模型,所以我們不必關心父子關係。如果我們的模型是層級模型,可能還要實現index()和parent()函式。
字串被儲存在內部的stringList私有成員變數中。
模型的維度
我們想要讓模型的行數和字串連結串列中的項數一樣。所以,我們如下實現rowCount()函式:
int StringListModel::rowCount(const QModelIndex &parent) const
{
return stringList.count();
}
因為該模型是非層級的,所以我們可以安全的忽略父項的模型下標。預設情況下,從QAbstractListModel類派生的模型只包含一列,所以我們也沒必要實現columnCount()函式。
模型頭資訊和資料的獲取
對於檢視中的項,我們想要返回字串列表中的字串。data()函式負責返回對應於index引數的項的資料。
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)
return stringList.at(index.row());
else
return QVariant();
}
我們只是在index有效,行數在有效範圍內,請求的角色是模型所支援的情況下返回一個有效的QVariant型別的變數。
還有一些檢視,例如QTreeView和QTableView,同資料一道還可以顯示一些頭部資訊。如果我們的模型被顯示在一個具有頭資訊的檢視中,並我們想在表頭顯示行號和列號。我們可以實現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);
}
再一次,我們在角色是模型所支援的情況下,返回一個有效的QVariant變數。並且,在返回具體的資料時我們也考慮了頭部的方向問題。
不是所有的檢視都顯示頭部資訊,還有一些檢視會隱藏它們。儘管如此,我們還是推薦你去實現headerData()函式,來為模型提供的資料提供一些相關的描述資訊。
一個模型項可以有多個角色,對於指定的不同的角色要返回不同的資料。在我們自定義的模型中只有一種角色,DisplayRole,所以我們為每一項返回資料而不關心其指定的角色。但是,我們還是可以將我們為DisplayRole提供的資料用於其他角色的,比如ToolTipRole,這樣,檢視在一個提供框中顯示一些關於當前項的資訊。
可編輯的模型
上面的只讀模型可以向用戶展示一些簡單的資訊,但是,對於很多應用程式來說,一個可編輯的列表模型是更有用的。我們可以通過修改為只讀模型實現的data()函式來使模型中的項可以被編輯,當然,還有提供另外兩個函式:flags()和setData()。我們先將下面的函式新增到類宣告中:
Qt::ItemFlags flags(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
代理會在建立編輯器之前檢測一個模型項是否可編輯。模型必須讓代理只讀它的項是可編輯的。我們可以通過為模型中的每一項返回一個正確的標誌來實現這個功能;在這種情況下,我們使能所有的模型項,並且使它們可以被選擇和編輯:
Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled;
return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
}
我們不需要知道代理具體是怎麼處理編輯過程的。我們只需給代理提供一個方式將資料設定會模型。這是通過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;
}
在這個模型中,我們使用函式提供的值來替換字串列表中對應位置的值。但是,我們必須確保下標是有效的,項的型別是正確的,角色是受支援的。按照慣例,我們強調角色是EditRole,因為這是標準代理支援的角色。但對於boolean值來說,你可以使用Qt::CheckStateRole角色和設定Qt::ItemIsUserCheckable標誌;然後,提供一個複選框來編輯資料。因為此處模型中底層資料是同一個角色,所以這些細節使它更容易與標準組件整合。
當資料被給設定後,模型必須讓檢視知道一些資料已經改變了。我們通過發射dataChanged()訊號完成這個功能。因為此處只有一項發生了變化,所以訊號攜帶的引數被限制為一個模型下標。
下面,來修改data()函式,加入Qt::EditRole的判斷:
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();
}
傳入和刪除行
模型中的行列數是可以被改變的。但在上面我們自定義的字串模型中,只有一列,所以我們只要實現插入和刪除行的函式即可。在類宣告中加入下面這些函式的宣告:
bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex());
bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex());
因為行在這個模型中就對應於列表中的字串,insertRows()函式就是想列表中指定位置前面插入一些空字串。插入的字串數量等於指定的行的數量。
父下標通常被用於決定這些行被插入到模型中哪個地方。在這個例子中,我們只有一個頂層列表,所以,我們只需將空字串插入列表即可。
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()函式來通知其他元件行數將要發生變化。這個函式指定將要插入的第一行和最後一行的行號,以及它們的父下標。修改字串列表之後,又呼叫endInsertRows()來完成操作和通知其他元件模型的維度發生了變化,返回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;
}
Qt中檢視控制元件
Qt的基於項的控制元件的名字就反應它們各自的作用:QListWidget提供了一個項的列表,QTreeWidget展示了一個多級的樹形結構,QTableWidget提供了項的表格展示。每一個類都繼承了QAbstractItemView類的行為,該類為項的選擇和頭資訊的管理提供了公共行為。
列表控制元件
單層級的項列表通常使用QListWidget和一系列的QListWidgetItem來展示。List Widget的構建和其他控制元件一樣:
QListWidget *listWidget = new QListWidget(this);
列表項可以在它們被構造時就直接加入列表控制元件:
new QListWidgetItem(tr("Sycamore"), listWidget);
new QListWidgetItem(tr("Chestnut"), listWidget);
new QListWidgetItem(tr("Mahogany"), listWidget);
也可以在構建列表項時不給它們指定父,而在以後的某個時候通過函式將它們插入到列表控制元件中:
QListWidgetItem *newItem = new QListWidgetItem;
newItem->setText(itemText);
listWidget->insertItem(row, newItem);
列表控制元件中的每一項可以顯示一個檔案標籤和一個圖示。文字的顏色和字型可以改變,從而為每一項提供一個自定義的外觀。Tooltips,status tips和“What's This?”幫助也是很容易配置的:
newItem->setToolTip(toolTipText);
newItem->setStatusTip(toolTipText);
newItem->setWhatsThis(whatsThisText);
預設情況下,列表中的項是按它們被插入的順序放置的。項列表可以根據Qt::SortOrder中給出的標準排序,從而產生一個按字母正序或逆序放置的列表:
listWidget->sortItems(Qt::AscendingOrder);
listWidget->sortItems(Qt::DescendingOrder);
樹形控制元件
樹或層級的列表是由QTreeWidget和QTreeWidgetItem類來提供的。樹形控制元件中的每一項都可以有它們自己的孩子,並可以顯示多列的資訊。樹形控制元件的建立和其他控制元件一樣:
QTreeWidget *treeWidget = new QTreeWidget(this);
在項被插入樹形控制元件之前,必須先設定列數。例如,我們可以定義兩列,併為每一列提供一個頭資訊:
treeWidget->setColumnCount(2);
QStringList headers;
headers << tr("Subject") << tr("Default");
treeWidget->setHeaderLabels(headers);
為每一列提供資訊的最簡單的辦法就是使用一個字串列表。對應更復雜的頭部來說,你可以建立一個樹項,按你希望的方式裝飾它,然後把它作為樹控制元件的頭。
樹形控制元件的頂層項以樹形控制元件為父控制元件進行建立。它們可以以任意的順序被插入,或者你可以通過在建立每一個樹項時指定父項的方式讓它們按一定的順序排列:
QTreeWidgetItem *cities = new QTreeWidgetItem(treeWidget);
cities->setText(0, tr("Cities"));
QTreeWidgetItem *osloItem = new QTreeWidgetItem(cities);
osloItem->setText(0, tr("Oslo"));
osloItem->setText(1, tr("Yes"));
QTreeWidgetItem *planets = new QTreeWidgetItem(treeWidget, cities);
樹形控制元件處理頂層項和其他更深層的項有一點不同。項可以從樹的頂層被刪除通過呼叫樹形控制元件的takeTopLevelItem(),但更底層的項是通過呼叫它們父項的takeChild()函式來刪除的。頂層的項是用過insertTopLevelItem()函式插入的,更底層的項是通過它們的父項的insertChild()來插入的。我們可以很容易的刪除樹形控制元件中的頂層項和底層項。只需要判斷一下這個項是否是頂層項即可,而這個資訊可以由每一個項的parent()函式提供。例如,下面的程式碼從樹控制元件中刪除一項:
QTreeWidgetItem *parent = currentItem->parent();