列表模型(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 中的列表模型用的是二維表結構,即由行和列組成,就像這樣:
把滑鼠移到某個項上,還能看到工具提示呢。
咱們給 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】鍵進入編輯狀態,文字方塊中的初始值就不會空白了。
好了,關於怎麼繼承列表模型的公共基礎類別的話題,咱們就扯到這兒了。