最近在做项目时,需要实现一个功能:
在一个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); }