#(46):視圖和委托
前面我們介紹了模型的概念。下面則是另外一個基本元素:視圖。在?model/view 架構中,視圖是數據從模型到最終用戶的途徑。數據通過視圖向用戶進行顯示。此時,這種顯示方式不必須同模型的存儲結構相一致。實際上,很多情況下,數據的顯示同底層數據的存儲是完全不同的。
我們使用`QAbstractItemModel`提供標準的模型接口,使用?`QAbstractItemView`提供標準的視圖接口,而結合這兩者,就可以將數據同表現層分離,在視圖中利用前面所說的模型索引。視圖管理來自模型的數據的布局:既可以直接渲染數據本身,也可以通過委托渲染和編輯數據。
視圖不僅僅用于展示數據,還用于在數據項之間的導航以及數據項的選擇。另外,視圖也需要支持很多基本的用戶界面的特性,例如右鍵菜單以及拖放。視圖可以提供數據編輯功能,也可以將這種編輯功能交由某個委托完成。視圖可以脫離模型創建,但是在其進行顯示之前,必須存在一個模型。也就是說,視圖的顯示是完全基于模型的,這是不能脫離模型存在的。對于用戶的選擇,多個視圖可以相互獨立,也可以進行共享。
某些視圖,例如`QTableView`和`QTreeView`,不僅顯示數據,還會顯示列頭或者表頭。這些是由`QHeaderView`視圖類提供的。在《[QFileSystemModel](http://www.devbean.net/2013/02/qt-study-road-2-qfilesystemmodel/)》一章的最后,我們曾經提到過這個問題。表頭通常訪問視圖所包含的同一模型。它們使用`QAbstractItemModel::headerData()`函數從模型中獲取數據,然后將其以標簽 label 的形式顯示出來。我們可以通過繼承`QHeaderView`類,實現某些更特殊的功能。
正如前面的章節介紹的,我們通常會為視圖提供一個模型。拿前面我們曾經見過的一個例子來看:
~~~
QStringList data;
data << "0" << "1" << "2";
model = new QStringListModel(this);
model->setStringList(data);
listView = new QListView(this);
listView->setModel(model);
QPushButton *btnShow = new QPushButton(tr("Show Model"), this);
connect(btnShow, SIGNAL(clicked()),
this, SLOT(showModel()));
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(btnShow);
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(listView);
layout->addLayout(buttonLayout);
setLayout(layout);
~~~
運行一下程序,這個界面十分簡單:
[](http://files.devbean.net/images/2013/03/numberlist-demo.png)
跟我們前面的演示幾乎一模一樣。現在我們有一個問題:如果我們雙擊某一行,列表會允許我們進行編輯。但是,我們沒辦法控制用戶只能輸入數字——當然,我們可以在提交數據時進行檢測,這也是一種辦法,不過,更友好的方法是,根本不允許用戶輸入非法字符。為了達到這一目的,我們使用了委托。下面,我們增加一個委托:
~~~
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const;
void setEditorData(QWidget *editor, const QModelIndex &index) const;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const;
};
~~~
正如前面所說,委托就是供視圖實現某種高級的編輯功能。不同于經典的 Model-View-Controller(MVC)模式,model/view 沒有將用戶交互部分完全分離。一般地,視圖將數據向用戶進行展示并且處理通用的輸入。但是,對于某些特殊要求(比如這里的要求必須輸入數字),則交予委托完成。這些組件提供輸入功能,同時也能渲染某些特殊數據項。委托的接口由`QAbstractItemDelegate`定義。在這個類中,委托通過`paint()`和`sizeHint()`兩個函數渲染用戶內容(也就是說,你必須自己將渲染器繪制出來)。為使用方便,從 4.4 開始,Qt 提供了另外的基于組件的子類:`QItemDelegate`和`QStyledItemDelegate`。默認的委托是`QStyledItemDelegate`。二者的區別在于繪制和向視圖提供編輯器的方式。`QStyledItemDelegate`使用當前樣式繪制,并且能夠使用 Qt Style Sheet(我們會在后面的章節對 QSS 進行介紹),因此我們推薦在自定義委托時,使用`QStyledItemDelegate`作為基類。不過,除非自定義委托需要自己進行繪制,否則,二者的代碼其實是一樣的。
繼承`QStyledItemDelegate`需要實現以下幾個函數:
* `createEditor()`:返回一個組件。該組件會被作為用戶編輯數據時所使用的編輯器,從模型中接受數據,返回用戶修改的數據。
* `setEditorData()`:提供上述組件在顯示時所需要的默認值。
* `updateEditorGeometry()`:確保上述組件作為編輯器時能夠完整地顯示出來。
* `setModelData()`:返回給模型用戶修改過的數據。
下面依次看看各函數的實現:
~~~
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem & /* option */,
const QModelIndex & /* index */) const
{
QSpinBox *editor = new QSpinBox(parent);
editor->setMinimum(0);
editor->setMaximum(100);
return editor;
}
~~~
在`createEditor()`函數中,parent 參數會作為新的編輯器的父組件。
~~~
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);
}
~~~
`setEditorData()`函數從模型中獲取需要編輯的數據(具有`Qt::EditRole`角色)。由于我們知道它就是一個整型,因此可以放心地調用`toInt()`函數。editor 就是所生成的編輯器實例,我們將其強制轉換成`QSpinBox`實例,設置其數據作為默認值。
~~~
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);
}
~~~
在用戶編輯完數據后,委托會調用`setModelData()`函數將新的數據保存到模型中。因此,在這里我們首先獲取`QSpinBox`實例,得到用戶輸入值,然后設置到模型相應的位置。標準的`QStyledItemDelegate`類會在完成編輯時發出`closeEditor()`信號,視圖會保證編輯器已經關閉,但是并不會銷毀,因此需要另外對內存進行管理。由于我們的處理很簡單,無需發出`closeEditor()`信號,但是在復雜的實現中,記得可以在這里發出這個信號。針對數據的任何操作都必須提交給`QAbstractItemModel`,這使得委托獨立于特定的視圖。當然,在真實應用中,我們需要檢測用戶的輸入是否合法,是否能夠存入模型。
~~~
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
editor->setGeometry(option.rect);
}
~~~
最后,由于我們的編輯器只有一個數字輸入框,所以只是簡單將這個輸入框的大小設置為單元格的大小(由`option.rect`提供)。如果是復雜的編輯器,我們需要根據單元格參數(由`option`提供)、數據(由`index`提供)結合編輯器(由`editor`提供)計算編輯器的顯示位置和大小。
現在,我們的委托已經編寫完畢。接下來需要將這個委托設置為`QListView`所使用的委托:
~~~
listView->setItemDelegate(new SpinBoxDelegate(listView));
~~~
值得注意的是,new 操作符并不會真的創建編輯器實例。相反,只有在真正需要時,Qt 才會生成一個編輯器實例。這保證了程序運行時的性能。
然后我們運行下程序:
[](http://files.devbean.net/images/2013/03/numberlist-editing-demo.png)
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)