最近在做項目時,需要實現一個功能:
在一個QTreeWidget中,隨意移動父節點或子節點的位置,但父節點和子節點不能互調。
用圖來舉例的話,大概是這個樣子:
父節點[114514,114517]可以用鼠標拖拽。
比如將114514拖拽到114516后,那么114514就跑到了114516后面。
然后針對子節點[191981,191983]:
可以像父節點一樣調換位置,也可以拖拽到其他父節點下面,成為其他父節點的子節點。
但父節點無法成為子節點,子節點也無法成為父節點。
本人首先學習了一下QDrag,然后沒學懂,
於是就自己重寫QTreeWidget的MousePressEvent,MouseMoveEvent以及MouseReleaseEvent來實現了。
大概的思路就是:
①在MousePressEvent中獲取要拖拽的項目,記為pSource;
②在MouseMoveEvent中顯示一個Label,用來更加顯性的表示拖拽;
③在MouseReleaseEvent中獲取拖拽到的項目,記為pTarget,然后將pSource插入到pTarget周圍。
整個Demo的代碼已上傳至github:
https://github.com/pinpeng/QtLittleObject.git
具體為其中的DragTreeWidget文件夾
下面是對整個代碼進行的簡單的說明。
代碼如下(在每個代碼塊后面會有簡短的說明幫助理解)
class DragTreeWidget : public QTreeWidget { Q_OBJECT public: DragTreeWidget(QWidget* parent = nullptr); private: //初始化函數 void initTreeWidget(); void mousePressEvent(QMouseEvent* ev) override; void mouseMoveEvent(QMouseEvent* ev) override; void mouseReleaseEvent(QMouseEvent* ev) override; private: //用來顯示label,顯性表示拖拽 QLabel label; //表示被拖拽的item QTreeWidgetItem* pSource; //表示被拖拽的item的父節點 QTreeWidgetItem* pParent; //表示被拖拽的item在父節點中的索引 int originIndex; //用於判斷在mouseMoveEvent中是否已經移除了組件 bool isJudged; };
頭文件,沒什么好說的。
void DragTreeWidget::initTreeWidget() { for(int i=0;i<10;i++){ QTreeWidgetItem* pTopItem = new QTreeWidgetItem(this); pTopItem->setText(0,QString::number(114514+i)); addTopLevelItem(pTopItem); } for(int i=0;i<10;i++){ QTreeWidgetItem* pChildItem = new QTreeWidgetItem(topLevelItem(0)); pChildItem->setText(0,QString::number(191981+i)); topLevelItem(0)->addChild(pChildItem); } }
用來插入一些item用來進行實驗。
DragTreeWidget::DragTreeWidget(QWidget *parent) :QTreeWidget(parent), label(this), pSource(nullptr) { initTreeWidget(); setHeaderHidden(true); label.resize(100,30); label.setText(""); label.hide(); }
類構造函數,隱藏了QTreeWidget的頭部,設置label大小並隱藏label。
void DragTreeWidget::mousePressEvent(QMouseEvent *ev) { isJudged = false; pSource = itemAt(ev->pos()); if(pSource!=nullptr){ pParent = pSource->parent(); label.setText(pSource->text(0)); } QTreeWidget::mousePressEvent(ev); }
在mousePressEvent之中,主要是確定被拖拽的目標。
因為如果這個時候就將item從列表中take出來的話,那么將無法實現將列表展開的功能。
所以具體將列表take出來的行為,放入到mouseMoveEvent之中進行。
然后,這就導致了mouseMoveEvent中的下一個坑(悲
void DragTreeWidget::mouseMoveEvent(QMouseEvent *ev) { if(pSource==nullptr){ QTreeWidget::mouseMoveEvent(ev); return; } label.show(); label.move(ev->pos()); if(isJudged==false){ isJudged = true; //將item從parent中脫離出來 //並且記錄原始的位置,方便后面復原 if(pParent){ originIndex = pParent->indexOfChild(pSource); pParent->takeChild(originIndex); } else{ originIndex = indexOfTopLevelItem(pSource); takeTopLevelItem(originIndex); } } }
因為需要在mouseMoveEvent之中將被拖拽的目標給take出來,所以會出現一個問題:
mouseMoveEvent並不是只執行一次,而是每隔一段時間就會執行一次,也就是執行多次。
為了可能可以節省的效率,所以這里用了isJudge來判斷,避免每次調用時都執行一次takeChild/takeTopLevelItem函數。
void DragTreeWidget::mouseReleaseEvent(QMouseEvent *ev) { //無論如何都先隱藏一下label label.hide(); QTreeWidgetItem* pTarget = itemAt(ev->pos()); //如果拖動到沒有item的地方 if(pTarget==nullptr){ //如果是子節點,那么返回原位 if(pParent){ pParent->insertChild(originIndex,pSource); } //如果是父節點,那么移動到最下方 else{ addTopLevelItem(pSource); } } //如果拖動到子節點處 else if(pTarget->parent()){ //如果將子節點A拖動到子節點B處,那么將子節點A放置到子節點B的下方 if(pParent){ pTarget->parent()->insertChild( pTarget->parent()->indexOfChild(pTarget),pSource); } //如果將父節點A拖動到子節點B處,那么父節點A回歸到原位 else{ insertTopLevelItem(originIndex,pSource); } } //如果拖動到父節點處 else{ //如果將子節點A拖動到父節點B處,那么將子節點A添加到父節點B中 if(pParent){ pTarget->addChild(pSource); } //如果將父節點A拖動到父節點B處,那么將父節點A放置到父節點B的下方 else{ insertTopLevelItem(indexOfTopLevelItem(pTarget),pSource); } } //設置當前的焦點為pSource setCurrentItem(pSource); }
mouseReleaseEvent寫的比較臃腫冗長,因為其實按道理這地方應該分為幾個函數分開寫的,不過因為是demo,所以不管那么多了。
mouseReleaseEvent中大致做了如下幾件事:
①判斷被拖拽的item(pSource)、拖拽到的item(pTarget)是否有效
②判斷pSource、pTarget的類型
③根據類型(父類節點被拖拽到父類節點、子類節點被拖拽到子類節點、子類節點被拖拽到父類節點等等...)來執行對應的行為
整個源文件代碼如下:
DragTreeWidget::DragTreeWidget(QWidget *parent) :QTreeWidget(parent), label(this), pSource(nullptr) { initTreeWidget(); setHeaderHidden(true); label.resize(100,30); label.setText(""); label.hide(); } void DragTreeWidget::initTreeWidget() { for(int i=0;i<10;i++){ QTreeWidgetItem* pTopItem = new QTreeWidgetItem(this); pTopItem->setText(0,QString::number(114514+i)); addTopLevelItem(pTopItem); } for(int i=0;i<10;i++){ QTreeWidgetItem* pChildItem = new QTreeWidgetItem(topLevelItem(0)); pChildItem->setText(0,QString::number(191981+i)); topLevelItem(0)->addChild(pChildItem); } } void DragTreeWidget::mousePressEvent(QMouseEvent *ev) { isJudged = false; pSource = itemAt(ev->pos()); if(pSource!=nullptr){ pParent = pSource->parent(); label.setText(pSource->text(0)); } QTreeWidget::mousePressEvent(ev); } void DragTreeWidget::mouseMoveEvent(QMouseEvent *ev) { if(pSource==nullptr){ QTreeWidget::mouseMoveEvent(ev); return; } label.show(); label.move(ev->pos()); if(isJudged==false){ isJudged = true; //將item從parent中脫離出來 //並且記錄原始的位置,方便后面復原 if(pParent){ originIndex = pParent->indexOfChild(pSource); pParent->takeChild(originIndex); } else{ originIndex = indexOfTopLevelItem(pSource); takeTopLevelItem(originIndex); } } } void DragTreeWidget::mouseReleaseEvent(QMouseEvent *ev) { //無論如何都先隱藏一下label label.hide(); QTreeWidgetItem* pTarget = itemAt(ev->pos()); //如果拖動到沒有item的地方 if(pTarget==nullptr){ //如果是子節點,那么返回原位 if(pParent){ pParent->insertChild(originIndex,pSource); } //如果是父節點,那么移動到最下方 else{ addTopLevelItem(pSource); } } //如果拖動到子節點處 else if(pTarget->parent()){ //如果將子節點A拖動到子節點B處,那么將子節點A放置到子節點B的下方 if(pParent){ pTarget->parent()->insertChild( pTarget->parent()->indexOfChild(pTarget),pSource); } //如果將父節點A拖動到子節點B處,那么父節點A回歸到原位 else{ insertTopLevelItem(originIndex,pSource); } } //如果拖動到父節點處 else{ //如果將子節點A拖動到父節點B處,那么將子節點A添加到父節點B中 if(pParent){ pTarget->addChild(pSource); } //如果將父節點A拖動到父節點B處,那么將父節點A放置到父節點B的下方 else{ insertTopLevelItem(indexOfTopLevelItem(pTarget),pSource); } } //設置當前的焦點為pSource setCurrentItem(pSource); }