列表模型(Item Model),老周沒有翻譯為“項目模型”,因為 Project 和 Item 都可以翻譯為“項目”,容易出現歧義。乾脆叫列表模型。這個模型也確實是為數據列表準備的,它以 MVC 的概念為基礎,在原始數據和用戶界面視圖之間搭建橋梁,使兩者可以傳遞數據(提取、修改)。 Qt 裡面使用 ...
列表模型(Item Model),老周沒有翻譯為“項目模型”,因為 Project 和 Item 都可以翻譯為“項目”,容易出現歧義。乾脆叫列表模型。這個模型也確實是為數據列表準備的,它以 MVC 的概念為基礎,在原始數據和用戶界面視圖之間搭建橋梁,使兩者可以傳遞數據(提取、修改)。
Qt 裡面使用列表控制比較複雜,需要先創建模型(Model)。當然,也有像 QListWidget 類這樣已經封裝好,開箱即食的,這個後面再扯,現在咱們的重點是弄清楚 Item Model 是啥玩意兒。
這裡所說的 Item Model 並不是真正的數據,應該說算是個控制器。當用戶界面要顯示數據時,模型負責從原始數據那裡提取值,再把值傳到界面上呈現;如果用戶界面要修改數據,通過輸入框(QLineEdit等)輸入/修改內容,然後傳給模型,模型負責修改原始數據。這麼看來,視圖和原始數據不是直接通信的,模型就成了“中間商”。這個“中間商”可以不賺差價(按原始數據的樣子呈現),也可能賺差價(把原始數據加工一下再讓你看)。
列表模型有一個抽象基類,叫 QAbstractItemModel;對應地,視圖組件也有一個抽象基類,叫 QAbstractItemView。另外,在模型和視圖之間還有一個“代理人”,抽象基類叫 QAbstractItemDelegate,它幹嗎的呢?這是專業經紀人,負責門面工作。比如,在視圖組件里呈現數據時用什麼字體,什麼顏色來繪製文本,用什麼方式從模型提取數據等;在編輯數據時,有什麼控制項來輸入文本。以及在編輯結束後,輸入的內容怎麼傳給模型等。日常使用時咱們用到 QAbstractItemDelegate 不多,除非你自己想為數據項繪製 UI,或用自定義的編輯組件。如果只是改改外觀什麼的,還不如用 QSS 方便。
行了,不扯太遠了,咱們只要知道這幾個基類之間的關係就行了。咱們的重點還是放在 QAbstractItemModel 類上面。
QAbstractItemModel 有幾個純虛函數是必須在派生類中重寫的:
1、index 方法,聲明如下:
virtual QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
列表模型中的索引,專門用一個叫 QModelIndex 的類表示。index 方法是根據傳入的參數,返回 QModelIndex 對象。之所以要用 QModelIndex 類來表示列表項的索引,是因為它是由幾個值組成的:
a、行號;
b、列號;
c、父索引。
Qt 中的列表模型用的是二維表結構,即由行和列組成,就像這樣:
問:D在哪裡?
答:row = 1,column = 0。
每個項又可以包含父級節點和子級節點,但上面的二維表只有一層,沒有父級,所以它的 parent = QModelIndex()。用預設構造函數創建的 QModelIndex 表示無效索引,即行號是 -1,列號是 -1,無父無子。
綜上所言,D 的索引就是:row = 1,col = 0,parent = QModelIndex()。
這個模型真正可怕的地方在於,每個索引都有父、子級。於是你可以構想下麵這麼恐怖的列表:
Root是一個無效的索引,可以認為是頂層的”父級“。A、B、C、D、E、F 的父級都是 Root,行列號由0開始編排,A在第一行第一列,所以 row=0,col=0,parent=Root。E有子節點,即 M、N、O、P,然後MNOP的行號和列號也要從 0 重新計算,即 N 的索引是 row=0, col=1, parent=E。最後,Q 這廝又有子節點,是一個只有一行的列表:R、S、T。於是,RST的行列號也重新計算。即 R 的索引是 row=0, col=0, parent=Q。
不過,實際使用時,一般不需要構建這麼神的數據結構,而且這玩意放到用戶界面上還不知道怎麼顯示好呢。畢竟,咱們在界面上常見的視圖也就以下三種:
1)、多行,只有一列,這就相當於像數組這樣的數據了。用 QListView 組件來呈現;
2)、一級二維表,由行、列組成,由 QTableView 組件呈現;
3)、多級節點,典型的就是 QTreeView 組件了。Qt 的 TreeView 比 .NET 的控制項多了一個特點——可以在顯示多級節點的同時顯示表格。但要註意的是,只有首列才支持父子節點。所以,對於 QTreeView 視圖,構建這樣的數據也足夠了:
2、parent 方法。它的聲明如下:
virtual QModelIndex parent(const QModelIndex &child) const = 0;
返回 child 節點的父級節點,對於只有一層的列表,返回 QModelIndex() 即可。
3、rowCount 方法。聲明如下:
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
返回原始數據有總共有多少行。
4、columnCount 方法。它的聲明如下:
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
該方法返回原始數據有多少列,如果是數組之類的,返回 1。
5、data 方法。聲明如下:
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
這是個重要的成員,它要根據 index 參數指定的索引,返回數據項的值。這裡要說一下叫”數據角色“的概念。說通俗不易懂一點就是返回的值的用途。比如,role 參數的預設值指定了 DisplayRole,意思就是你返回的值是要顯示在用戶界面上的,就是你想讓用戶看到的文本。Qt::ItemDataRole 枚舉定義了一系列數據角色。
enum ItemDataRole { DisplayRole = 0, // 顯示在界面的內容 DecorationRole = 1, // 和文本一起顯示的圖標,類型一般是QIcon EditRole = 2, // 當編輯數據時,返回給用戶看的值 ToolTipRole = 3, // 顯示在工具提示中的文本 StatusTipRole = 4, // 顯示在狀態欄中的文本 WhatsThisRole = 5, // 幫助信息,顯示在”這是啥?“提示中 // Metadata FontRole = 6, // 呈現數據時用啥字體 TextAlignmentRole = 7, // 文本的對齊方式 BackgroundRole = 8, // 返回畫刷對象,用來繪製列表項的背景 ForegroundRole = 9, // 文本的顏色 CheckStateRole = 10, // 如果界面上顯示了 checkbox,那麼返回checkbox的狀態(選中?未選中?未知?) // Accessibility AccessibleTextRole = 11, // 簡練的輔助信息。用於像”講述人“這些輔助工具 AccessibleDescriptionRole = 12, //詳細輔助信息,用於像”講述人“類似的輔助工具 // More general purpose SizeHintRole = 13, // 返回該列表項希望顯示的大小(寬多少,高多少) InitialSortOrderRole = 14, // 數據第一次呈現時用的排序方式(升序?降序?) // 下麵五個不知道是什麼鬼 // Internal UiLib roles. Start worrying whe high. DisplayPropertyRole = 27, DecorationPropertyRole = 28, ToolTipPropertyRole = 29, StatusTipPropertyRole = 30, WhatsThisPropertyRole = 31, // Reserved,保留用來給開發者自定義角色。自定義角色從這個數值開始 UserRole = 0x0100 };
重寫 data 方法實現了數據的只讀模式,若數據支持編輯,必須重寫 setData 方法,把內容寫入原始數據。
如果要實現添加、刪除數據項,還要重寫以下方法:
6、insertRows:在行號為 row 處連續插入 count 行數據。
virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
7、removeRows:從行號為 row 處開始,連續刪除 count 行。
virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
8、insertColumns:從列號為 column 處起,連接插入 count 列。
virtual bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex());
9、removeColumns:從列號為 column 處開始,連續刪除 count 列。
virtual bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex());
===============================================================
下麵咱們做一個簡單的模型練練手。該模型的原始數據是一個整數列表(QList<int>)。先實現只讀功能,即需要重寫 parent、index、rowCount、columnCount 和 data 方法。頭文件的聲明如下:
#ifndef MODELS_H #define MODELS_H #include <QAbstractItemModel> #include <QObject> #include <QList> class MyItemModel: public QAbstractItemModel { Q_OBJECT public: // 下麵兩個是構造函數 explicit MyItemModel(QObject* parent = nullptr); explicit MyItemModel(const QList<int> &list, QObject* parent = nullptr); // 返回父級 QModelIndex parent(const QModelIndex & child) const override; // 返回索引 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; // 返回行數和列數 int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; // 獲取數據 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; // 下麵這兩個方法用來獲取或設置數據源 QList<int> intList() const; void setIntList(const QList<int> &list); private: QList<int> m_list; // 私有的 }; #endif
私有欄位 m_list 用於引用原始數據,可以通過構造函數的 list 參數設置,也可以通過 setIntList 方法設置。
下麵代碼實現構造函數,主要是初始化私有成員。
MyItemModel::MyItemModel(QObject *parent) : m_list(QList<int>()), QAbstractItemModel(parent) { } MyItemModel::MyItemModel(const QList<int> &list, QObject *parent) : QAbstractItemModel(parent), m_list(list) { }
下麵代碼實現 parent 方法。由於是整數列表,數據只有一層,所以直接返回 QModelIndex() 即可。
QModelIndex MyItemModel::parent(const QModelIndex &child) const { // 簡單的列表不需要父子層次 // 使用無參構造函數表示無效索引 return QModelIndex(); }
接著是實現返回數據列表的行數和列數。
int MyItemModel::rowCount(const QModelIndex &parent) const { // 一樣的道理,不能有父級數據 if (parent.isValid() ){ return 0; } // 返回QList中元素個數,每個元素代表一行 return m_list.size(); } int MyItemModel::columnCount(const QModelIndex &parent) const { if(parent.isValid()) return 0; // 咱們這個模型永遠只有一列 return 1; }
實現 index 方法,為數據項創建索引。
QModelIndex MyItemModel::index(int row, int column, const QModelIndex &parent) const { // 因為此列表不存在爺爺/孫子/父/子關係 // 所以如果索引是有效的,說明它不對 // 咱們這個列表是沒有父級的 if(parent.isValid()) return QModelIndex(); // 有效索引不是咱們想要的,返回無效索引 // 如果索引無效,說明是頂層數據,是咱們想要的 return createIndex(row, column); }
QModelIndex 無法直接訪問其成員,要產生索引請調用 createIndex 方法。
實現 data 方法。返回數據,這裡咱們實現了正常顯示的文本和作為工具提示用的文本。
QVariant MyItemModel::data(const QModelIndex &index, int role) const { // 註意 role 這個參數,返回前必須判斷 if(role == Qt::DisplayRole) { // DisplayRole 說明獲取的數據是用在界面呈現上的 // 咱們只考慮行號,列號嘛,反正只有一列 int idx = index.row(); return m_list.at(idx); } // 可以提供工具提示 if(role == Qt::ToolTipRole) { int i = index.row(); int val = m_list.at(i); return QString("這是整數值:%1").arg(val); } // 如果是其他role,就返回一個預設的QVariant給它 return QVariant(); }
其他未用到的數據角色返回空的 QVariant 就可以。data 方法返回的值,是對應著二維表中某個單元格的,所以,你希望在那個單元格中顯示什麼就返回什麼。
最後實現的兩個方法是用來獲取或設置數據源的(即原始數據)。
QList<int> MyItemModel::intList() const { return m_list; } void MyItemModel::setIntList(const QList<int> &list) { m_list = list; }
至此,一個簡單的模型就有了,當然,沒有實現 setData 方法,它只能讀數據,不支持編輯。現在我們可以拿來用了。
int main(int argc, char** argv) { QApplication app(argc, argv); // 創建視圖實例 QListView lv; lv.setWindowTitle("簡單模型"); // 準備點數據 QList<int> theList; theList << 100 << 300 << 4500 << 600 << 1200; // 實例化模型 MyItemModel *model; model = new MyItemModel(theList); //model->setIntList(theList); // 為視圖設置模型 lv.setModel(model); // 顯示視窗 lv.show(); // 主迴圈 return QApplication::exec(); }
QListView 作為視圖組件,適合顯示簡單的列表。調用視圖的 setModel 方法就可以關聯指定的模型對象了。如上述代碼中,咱們自定義的 MyItemModel 在設置了原始數據後,就可以傳遞給 setModel 方法以提供數據。
結果如下圖所示:
把滑鼠移到某個項上,還能看到工具提示呢。
咱們給 MyItemModel 加上 setData 方法的重寫,使它支持編輯功能。
// 頭文件 bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex &index) const override;
// 實現代碼 bool MyItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { // 設置數據時數據角色通常是編輯 if(role == Qt::EditRole) { // 因為只有一列,我們不用關心列號,只取行號 int row = index.row(); if(value.canConvert<int>() == false) { // 不是int值,玩不下去了 return false; } // 更新數據 m_list.replace(row, value.toInt()); // 發出信號 QList<int> roles = { Qt::DisplayRole, Qt::EditRole, Qt::ToolTipRole }; emit dataChanged(index, index, roles); // 輸出一下,主要是檢查list有沒有順利修改 qDebug() << m_list; return true; } return false; } Qt::ItemFlags MyItemModel::flags(const QModelIndex &index) const { Qt::ItemFlags oldFlags = QAbstractItemModel::flags(index); return oldFlags | Qt::ItemIsEditable; }
先說說為什麼要同時重寫 flags 方法,此方法返回 ItemFlag 枚舉的值(值可以合併)。如果想讓視圖組件知道此模型允許編輯,那麼返回的 ItemFlags 必須包含 ItemEditable 值。
現在說 setData 方法。首先,role 參數得是 EditRole 才表明用戶界面已進入編輯狀態,並且這個值是在編輯狀態下傳送過來的。canConvert 方法是檢查一下傳過來的是不是 int 值,這裡如果是 QListView 組件預設處理的話,一般不會搞錯類型。
咱們的原始數據就是存放在 QList<int> 對象中的,所以只調用 replace 方法把某個索引處的值替換下就可以了;如果數據來自文件,就得寫入文件以保存。
在數據更新後,記得發送一個 dataChanged 信號,通知所有連接到此信號的對象,數據已變更,趕緊刷新提取最更的值。dataChanged 信號需要三個參數:
void dataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles = QList<int>());
topLeft 參數和 bottomRight 參數是兩個索引,它們描述了被修動數據的區域,用左上角和右下角的索引來表示。本示例中,每次只修改一個行,所以,左上角和右下角的索引都是被修改項的索引。roles 參數告訴程式:哪些角色的數要更新一下。一般 EditRole 和 DisplayRole 的要更新,這樣可讓應用程式知道去刷新數據。模型只用在 QListView 視圖中,所以就算不發出 dataChanged 信號,組件也能自動刷新。但如果模型同時應用在多個視圖中,並且有其他代碼連接了 dataChanged 信號,那就得發出這個信號了。
setData 方法返回 bool 值,true 表示成功,false 表示失敗。
修改後,只要雙擊列表項,就會出現文本框,然後你可以輸入新的值,輸完後按“回車”鍵,或者移開焦點(如點擊其他空白地方),就會觸發更新。
但是,你會發現一個問題:進入編輯狀態時,文本框里都是空的。如下圖:
這不合理,應該顯示原有的值讓用戶修改。造成編輯狀態下初始值空白的原因是咱們前面的 data 方法。因為咱們在返回值的時候,只判斷了在 DisplayRole 角色下才返回,當視圖進入編輯狀態後,調用 data 方法獲取數據時,role 參數的值是 EditRole,這就導致獲取到空值。
回去修改一下 data 方法的代碼。
QVariant MyItemModel::data(const QModelIndex &index, int role) const { // 註意 role 這個參數,返回前必須判斷 if(role == Qt::DisplayRole || role == Qt::EditRole) { …… } …… }
現在,雙擊列表項或按【F2】鍵進入編輯狀態,文本框中的初始值就不會空白了。
好了,關於怎麼繼承列表模型的公共基類的話題,咱們就扯到這兒了。