Qt模型/檢視原理(2):自定義模型
Qt模型/檢視原理(2):自定義模型
若對C++語法不熟悉,建議參閱《C++語法詳解》一書,電子工業出版社出版,該書語法示例短小精悍,對查閱C++知識點相當方便,並對語法原理作了詳細講解。
自定義模型至少需要實現QAbstractItemModel類中的以下5個純虛擬函式
columnCout()、rowCount()、index()、parent()、data()
為了能新增自已的資料到模型中,通常還需要重新實現setData()函式,若不重新實現setData()則無法向模型中新增資料。
自定義模型的基本原理及步驟如下
①、資料:實際資料可使用QList、陣列、整型、或單獨的一個類來儲存,資料可存放在模型中,也可存放在檔案等其他地方。
②、columnCout()、rowCount()、index()、parent()這4個函式用於共同設計模型的結構,因為使用索引表示模型中的某個資料項的位置,因此設計模型索引的結構就是設計模型的結構。具何如下
行數和列數的設計:比如對於3行4列的表格結構,columnCout()應返回4,rowCount()應返回3;對於列表結構,則因為列表只需要1列,所以columncout()應總是返回1,rowCount()返回該列表的行數;對於樹形結構模型,則更復雜,需要根據當前父節點的情況進行判斷,以返回該父節點擁有的列數和行數。
parent()函式(父模型索引)的設計:因為表格結構中的所有單元格都屬於同一個父索引之下,所以可把所有單元格都視為頂級節點,因此他們的父索引可以以無效模型索引作為父索引,因此parent()可以返回一個無效模型索引;對於列表結構的模型,同樣只需返回一個無效模型索引即可;對樹形結構模型,此步驟比較複雜,可以通過獲取當前節點的父節點及其行號和列號,然後使用createIndex()建立該父節點的索引。
index()函式的設計:該函式用於為模型中的每個資料項建立索引,建立索引需要使用createIndex()函式,對於表格結構,只需向createIndex()函式傳遞當前資料項所在的行號、列號及使用的資料的指標即可;對於列表結構,則列號始終為0,其餘同表格結構;對於樹形結構,需要向該函式傳遞當前資料項位於父索引中的行號、列號及使用的資料的指標。
③、data()函式的返回值決定了檢視上應顯示的資料,也就是說在介面上使用者看到的資料是由該函式返回的,若返回不當的值,則資料無法正常顯示在檢視上,下面以使用內建的標準檢視類為例來講解怎樣設計此函式。data()函式會被檢視類呼叫多次,檢視每次都會向data傳遞一個不同的role(角色)引數值,然後檢視根據data返回的值,設定該role的資料, 因此在設計data函式的返回值時,需要根據role的不同值返回不同的資料,以使檢視正確的顯示。
示例8.4:自定義表格模型 注:本小節使用的QList的幾個成員函式,請參閱幫助文件,另外QModelIndexList是使用typedef重新命名後的QList。 //m.h檔案內容 #ifndef M_H #define M_H #include<QtWidgets> #include <iostream> using namespace std; class D:public QAbstractItemModel{ //繼承抽象類QAbstractItemModel public: int col,rw; //表格模型的列數和行數 QList<QVariant> s1; //這是模型管理的資料 QList<int> rol; //儲存資料的角色 //讀者可把以一兩項單獨封裝在一個類(比如xxxItem)中進行管理,Qt內建模型就是這樣處理的 //建構函式表示建立一個i行j列的表格模型 D(int i, int j):rw(i),col(j){ for(int k=0;k<col*rw;k++){ s1<<QVariant(); rol<<-1;} } //①、返回表格模型的行數 int rowCount(const QModelIndex &parent = QModelIndex ()) const{return rw; } //②、返回表格模型的列數 int columnCount(const QModelIndex &parent =QModelIndex ()) const{return col;} //③、返回表格模型的父索引,因為所有單元格都是頂級節點,所以使用無效節點作為父節點 QModelIndex parent(const QModelIndex &index) const{ return QModelIndex();} //④、為每個單元格建立一個唯一的索引 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex ()) const { cout<<"index"<<endl; //測試語句 //其他模型可能需要判斷傳遞進來的索引是否有效。 //if(!hasIndex(row,column,parent))return QModelIndex(); /*本示例僅需簡單的為傳遞進來的單元格建立一個索引即可,注意第3個引數的使用(請參閱createIndex()的原型),由此處可見,第3個引數其實質就是指指向模型實際所管理的資料。*/ return createIndex(row, column, (void*)&(s1.at(row*column+column))); } //⑤、返回檢視上顯示的資料,該函式會被檢視多次呼叫(注:其他虛擬函式同樣會被Qt呼叫多次) QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const{ //測試用,讀者可看到每次呼叫時Qt內部傳遞進來的role(角色)的值。 cout<<"data="<<role<<endl; int i=index.row()*col+index.column(); //單元格所在資料s1中的位置 //以下對不同角色返回的資料會被檢視使用,以正確的顯示在其單元格中。 /*對於QListView和QTreeView必須對所在節點設定大小,否則這兩個檢視不會顯示任何內容(因為節點大小為0)*/ //if(role==Qt::SizeHintRole)return QSize(55,55); /*設定角色CheckStateRole的資料。本示例返回一個無效的QVariant作為該角色的資料, 若返回有效值,會使單元格的左側顯示一個複選框。*/ if(role==Qt::CheckStateRole)return QVariant(); //設定單元格中資料的對齊方式。本示例為左側垂直居中對齊 if(role==Qt::TextAlignmentRole) return Qt::AlignLeft|Qt::AlignVCenter; //設定角色DecorationRole(圖片)的資料 if(role==Qt::DecorationRole) //若使用者設定了DecorationRole角色的資料,則返回使用者為該單元格設定的資料。 if(rol.at(i)==Qt::DecorationRole) return s1.at(i); //若使用者為角色EditRole或DisplayRole角色設定了資料,則返回使用者為該單元格設定的資料。 if(role==Qt::EditRole|role==Qt::DisplayRole) if(rol.at(i)==Qt::EditRole|rol.at(i)==Qt::DisplayRole) return s1.at(i); //其餘角色使用無效資料 return QVariant(); } //data()結束 //⑥、過載setData以使使用者可以向模型中新增資料 bool setData(const QModelIndex &index,const QVariant &value, int role=Qt :: EditRole) {/*使用資料value和角色role分別替換列表s1和rol中原有的值,使用replace便於下一個示例(拖放)的使用。*/ s1.replace(index.row()*col+index.column(),value); rol.replace(index.row()*col+index.column(),role); emit dataChanged(index, index); //資料改變後,需要傳送此訊號。 return true; } }; //返回true,表示資料設定成功。 #endif // M_H //m.cpp檔案內容 #include "m.h" int main(int argc, char *argv[]){ QApplication aa(argc,argv); D *d=new D(3,3); //建立一個3行3列的表格模型 QTableView *pv2=new QTableView; //使用表格檢視來顯示以上模型管理的資料 /*讀者也可使用以下檢視來顯示模型d中的資料,但要去掉data()中對if(role==Qt::SizeHintRole)的註釋,還要注意,若使用樹形檢視顯示,有可能會使程式崩潰,因為本示例未完整的實現樹形模型結構*/ //QListView *pv2=new QListView; //QTreeView *pv2=new QTreeView; //向模型中新增資料 d->setData(d->index(0,0,QModelIndex()),"111",Qt::DisplayRole); d->setData(d->index(1,0,QModelIndex()),222); d->setData(d->index(1,1,QModelIndex()),333); d->setData(d->index(2,1,QModelIndex()),QIcon("F:/1i.png"),Qt::DecorationRole); pv2->setModel(d); pv2->resize(333,222); pv2->show(); return aa.exec(); }
執行結果及說明,見圖8-16,由圖可見,資料顯示正確
示例8.5:自定義拖放操作 注:本示例需在上一示例的main函式中增加以下程式碼(啟用檢視的拖放功能)。本示例的函式原型及其作用見示例後文對這些函式的講解。 pv2->setDragEnabled(true); pv2->viewport()->setAcceptDrops(true); pv2->setDropIndicatorShown(true); pv2->setDragDropMode(QAbstractItemView::DragDrop); 再在類D之中增下如下函式的定義 //①、重新實現flags函式,以開啟模型的拖放功能 Qt::ItemFlags flags(const QModelIndex &index) const{ return Qt::ItemIsEditable|Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled| Qt::ItemIsEnabled|Qt::ItemIsSelectable; } //②、重新實現supportedDropActions以使模型即可複製又可移動 Qt::DropActions supportedDropActions() const{return Qt::CopyAction|Qt::MoveAction;} //③、重新實現canDropMimeData函式,以決定單元格是否允許放置拖動過來的資料。 bool canDropMimeData(const QMimeData *data,Qt::DropAction action, int row, int column, const QModelIndex &parent) const { /*本示例parent引數有效,row和column是無效的,以下程式碼表示,第3行第3列不接受來自拖放的資料。*/ if(parent.row()==2&&parent.column()==2) return false; else return true; } //④、重新實現mimeData函式,以編碼拖動時的資料(就是把拖動時的資料儲存起來,並返回) QMimeData* mimeData(const QModelIndexList &indexes) const{ QMimeData *const pm=new QMimeData(); //建立一個QMimeData物件。 /*注意:indexes是被拖動的資料項的索引,若同時拖動了多個數據項,則indexes才會包含多個數據項。*/ for(int k=0;k<indexes.size();k++){ const QModelIndex &i=indexes.at(k); if (i.isValid()) { QByteArray t; //提取資料項的索引為i,角色為DisplayRole的資料 QVariant v=data(i, Qt::DisplayRole); t.append(v.toString()); //把提取的資料儲存在t之中 pm->setData("text/plain", t); //然後把t中的資料以MIME型別的形式編碼到pm中。 } //if結束 } //for結束 return pm; } //返回編碼後的資料 //⑤、重新實現dropMimeData函式,以解碼拖動時的資料並對其進行處理 bool dropMimeData(const QMimeData *data, Qt::DropAction action,int row, int column, const QModelIndex &parent) {/* 本示例parent引數有效,row和column無效,以下程式碼表示儲存單元格所在資料s1中的位置。索引parent是拖動之後需要放置的位置的索引。*/ int i=parent.row()*col+parent.column(); QByteArray t=data->data("text/plain"); //解碼由mimeData函式編碼後的資料。 //若是複製操作,則把新位置parent處的資料重置為t if(action==Qt::CopyAction) { setData(parent,t); } //若是移動操作,則把新位置parent處的資料重置為t if(action==Qt::MoveAction) { setData(parent,t); } return true; } //返回true表示資料已處理。
執行結果及說明見圖8-17
8.2.3 拖放支援及MIME型別
拖放操作分為拖動(Drag)和放下或放置(Drop)兩種操作,當拖動時,需要把拖動的資料進行儲存,儲存資料這一步驟稱為編碼,資料以MIME型別(見6.6節)儲存為QMimeData型別的物件(稱為放置資料),當執行放下操作時,需要對放置資料進行處理,若需要使用這些資料,則需要把儲存的資料讀取出來(即解碼),然後進行處理;當然,若不需要這些放置資料,也可以直接丟棄。
1、自定義拖放操作的步驟
(1)、啟用檢視的拖放支援
標準檢視自動支援內部拖放,預設情況下,這些檢視的拖放功能未啟用,要啟用檢視的拖放支援,需進行以下設定:
設定檢視的dragEnabled屬性為true以啟用專案拖放。
拖放的模式:即檢視是否支援拖放,是否接受放置資料,是否接受來自自身的移動操作等。需要設定檢視的dragDropMode屬性。
若要允許使用者在檢視內放置內部或外部專案,需將檢視的viewport()的acceptDrops屬性設定為true。
若要向用戶顯示拖動的專案放置位置(通常該位置會以不同的形式顯示,比如以陰影形式顯示等),此時需設定檢視的showDropIndicator屬性。
下面為具體的程式碼
v->setDragEnabled(1); //啟用檢視的拖放支援
v->setAcceptDrops(1); //接受放置資料
v->setDropIndicatorShown(1); //顯示放置位置
v->setDragDropMode(QAbstractItemView::DragDrop); //設定拖放模式為支援拖放
(2)、啟用資料項的拖放支援
重新實現QAbstractItemModel::flags()函式以提供合適的標誌來指示哪些專案可以被拖動,哪些專案將接受放置(Drop)。
(3)、編碼資料
重新實現QAbstractItemModel::mimeData()函式,把編碼後的資料儲存在該函式返回的QMimeData物件中。
(4)、 處理放置資料
1)、內建模型對放置資料的處理
不同型別的模型以不同的方式處理放置資料。列表模型和表格模型只提供了儲存資料項的平面結構。因此,他們可能會在資料放入檢視中的現有專案時插入新行(和列),或者可能會使用提供的一些資料覆蓋模型中專案的內容。樹模型通常能夠將包含新資料的子項新增到其基礎資料儲存中,因此就使用者而言,其行為將更具可預測性。
2)、對放置資料的自行處理
通過重新實現QAbstractItemModel::dropMimeData()函式來處理放置資料,此時需要對放置資料進行解碼(即讀取出QMimeData物件儲存的資料的內容),並將其插入模型的底層資料結構中(或進行其他處理),若該函式修改了資料項或模型的尺寸,則必須注意確保發出所有相關的訊號。因為該函式需要插入或刪除等操作,所以簡單的呼叫QAbstractItemModel子類中的已經實現了的setData(),insertRows()和insertColumns()等函式會更方便。另外,還可以使用dropMimeData()函式的預設實現來處理放置資料,dropMimeData()函式的預設實現不會覆蓋模型中的任何資料,它會將放置資料作為專案的同胞插入,或者作為該專案的子項插入。若要使用該函式的預設實現,就需要重新實現以下函式:insertRows()、insertColumns()、setData()、setItemData()。
2、自定義拖放操作的其他設定
(1)、模型能接受的拖放操作的型別
重新實現QAbstractItemModel::supportedDropActions() 函式可以指示模型能接受的拖放操作的型別(比如移動、複製等),雖然可以給出Qt::DropActions的值的任意組合,但需要編寫程式碼來支援它們,比如,對於Qt::MoveAction(移動)操作,則模型就需要提供QAbstractItemModel :: removeRows()的實現。
(2)、對多種MIME型別的支援
預設情況下,內建模型和檢視只支援一種MIME型別,即
application/x-qabstractitemmodeldatalist。
若要讓模型支援多種MIME型別,則需要重新實現QAbstractItemModel::mimeTypes(),該函式返回拖放操作時模型可以處理的MIME型別列表,注意,自定義資料型別必須宣告為元物件。
(3)、禁止在專案上放置資料
重新實現QAbstractItemModel:: canDropMimeData()函式可禁止在專案上放置資料。
3、下面為相關的函式及其原型(QAbstractItemModel類中的成員函式)
18)、
virtual Qt::ItemFlags flags(const QModelIndex &index) const; //虛擬的
返回索引index的資料項的標誌組合,預設實現為Qt::ItemIsEnabled | Qt::ItemIsSelectable,重新實現該函式可控制資料項的屬性(比如是否可被選中,可拖動等)。Qt::ItemFlag列舉見表8-2
19)、virtual QMimeData* mimeData(const QModelIndexList &indexes) const; //虛擬的
返回一個包含與索引列表indexes相對應的序列化資料項。若indexes為空或沒有支援的MIME型別,應返回0而不是序列化的空列表。該函式用於編碼拖放的資料。
20)、bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); //虛擬的
處理以動作action結束的拖放操作提供的資料data,即解碼放置資料data並進行處理。若資料已由模型處理,則返回true,否則返回false。引數row、column、parent表示操作結束時資料項的位置。Qt::DropAction列舉見表8-3
該函式的預設實現不會覆蓋模型中的任何資料,它會將放置資料作為專案的同胞插入,或者作為該專案的子項插入,比如:對QTreeView中的資料項執行放置操作時,可能會導至新資料項被插入到row、column、parent所指資料項的子項,或作為該資料項的兄弟項。當row和column為-1時,通常意味著把資料項追加為parent的子項,若row和column大於或等於0,則意味著應在指定的row和column之前發生。
該函式會呼叫mimeTypes()函式,以獲取可接受的MIME型別的列表。
21)、virtual bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const; //虛擬的
若模型可以接受放置資料data,則返回true。預設實現僅檢查data是否在mimeTypes()列表中至少有一種格式,以及action是否在模型的supportedDropActions()中。若要在row、column、parent上測試資料是否可以放置(drop),則需要重新實現此函式,若不需要進行此種測試,就不必重新實現該函式。Qt::DropAction列舉見表8-3
22)、virtual QStringList mimeTypes() const; //虛擬的
返回允許的MIME型別列表,預設情況下,內建模型和檢視使用的MIME型別為:application/x-qabstractitemmodeldatalist。若在自定義模型中實現拖放支援時,需要處理預設型別以外的格式,則需要重新實現此函式以返回需要的MIME型別列表。若重新實現此函式,還必須重新實現呼叫該函式的mimeData()和dropMimeData()函式。
23)、virtual Qt::DropActions supportedDragActions() const; //虛擬的
返回模型中資料支援的動作,預設實現返回supportedDropActions()。當發生拖放時,該函式的返回值被用作QAbstractItemView::startDrag()的預設值。
24)、virtual Qt::DropActions supportedDropActions() const; //虛擬的
返回此模型支援的放置動作,預設返回Qt::CopyAction。若重新實現此函式,還必須重新實現dropMimeData()函式以處理其他動作。Qt::DropAction列舉見表8-3
本文作者:黃邦勇帥(原名:黃勇)