自己的一个项目想要实现在布局中的控件可以随意通过鼠标拖拽改变控件的位置,经过一晚上的资料查找和实际操作终于完成了可以通过拖拽布局中的控件改变控件在布局中的位置的功能。本文拖拽线的代码实现参考了一位大佬的博客而获得的灵感,其主要以C++ Qt实现的功能。
大佬博客链接>>>Qt控件拖拽分割线
=========================================================================
1.案例要求
(1) 控件能够在垂直布局和水平布局中自由移动
(2) 拖拽出去控件的透明
(3) 拖到其他控件边缘的时候要有标识线提示
2.案例实现想法
(1)控件在布局中自由移动
使用QDrag实例来承载拖动中控件,在拖出去的时候控件实际上仍然保留着在布局中原有的位置,当选定好要插入的位置后,布局实例主动将控件从原位置中提出并插入到新的位置中。
(2)拖拽出去的控件透明
在设定QDrag的承载QPixmap的时候将QPixmap填充为透明度只有0.5的白板,并且设定Painter的绘制透明度为0.5即可。
(3)拖到其他控件边缘的时候有标识线
首先先设定好一个QLabel对象,设定好QLabel对象的填充颜色并将其隐藏,拖拽控件的时候,使用dragMoveEvent函数监听鼠标移动事件,当鼠标位置处于所处控件的边缘的时候,将设定好的标示线对象(QLabel)移动到指定的控件边缘位置,大小设定为贴合控件边缘的大小并显示,当鼠标移出后又将标示线对象(QLabel)隐藏即可。
3.完整代码
import sys
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget,QApplication
from PyQt5.QtCore import QMimeData,Qt
from PyQt5.QtGui import QDrag,QPainter,QPixmap,QColor
class DragBaseLayout(QWidget):
def __init__(self,parent = None):
super(DragBaseLayout,self).__init__(parent)
self.setAcceptDrops(True) # 必须开启接受拖拽
self.setLayoutType(QVBoxLayout) # 设置布局类型并且设定好布局
self.createComponent() # 生成控件
self.assembleComponent() # 组装控件
# 设定控件的layout类型
def setLayoutType(self,Alayout):
self.layout = Alayout()
self.setLayout(self.layout)
# 初始化内部的控件
def createComponent(self):
self.lb1 = QLabel()
self.lb1.setPixmap(QPixmap("A.png"))
self.lb2 = QLabel()
self.lb2.setPixmap(QPixmap("B.png"))
self.lb3 = QLabel()
self.lb3.setPixmap(QPixmap("C.png"))
self.line = LineLable(self)
def assembleComponent(self):
self.layout.addWidget(self.lb1)
self.layout.addWidget(self.lb2)
self.layout.addWidget(self.lb3)
# 自定义插入控件的函数
def addWidget(self,widget):
self.layout.addWidget(widget)
# 设定鼠标按下事件
def mousePressEvent(self,event):
item = self.childAt(event.pos()) # 通过位置获得控件
if item == None:return # 如果为空则直接跳过
index = self.layout.indexOf(item) # 获得当前控件所处的布局中的索引位置
self.drag = QDrag(item) # 创建QDrag对象
mimedata = QMimeData() # 然后必须要有mimeData对象,用于传递拖拽控件的原始index信息
mimedata.setText(str(index)) # 携带索引位置信息
self.drag.setMimeData(mimedata)
pixmap = QPixmap(item.size())
pixmap.fill(QColor(255,255,255,0.5)) # 绘制为透明度为0.5的白板
painter = QPainter(pixmap)
painter.setOpacity(0.5) # painter透明度为0.5
painter.drawPixmap(item.rect(),item.grab()) # 这个很有用,自动绘制整个控件
painter.end()
self.drag.setPixmap(pixmap)
self.drag.setHotSpot(event.pos()-item.pos())
self.drag.exec_(Qt.MoveAction) # 这个作为drag对象必须执行
# 拖拽移动事件(通过实时监测鼠标的位置,并根据位置决定标志线是否显示)
def dragMoveEvent(self,event):
point = event.pos()
currentItem = self.childAt(point)
if type(currentItem)==QLabel:
geometry = currentItem.geometry()
if type(self.layout) == QHBoxLayout:
self.Hline(point,geometry,currentItem)
elif type(self.layout) == QVBoxLayout:
self.Vline(point,geometry,currentItem)
else:
self.line.hide()
# 拖拽放下事件
def dropEvent(self,event):
point = event.pos() # 获得落点的坐标
otherItem = self.childAt(point) # 获得当前落点上的控件
if type(otherItem)==QLabel:
if type(self.layout) == QHBoxLayout:
self.HInsert(point,otherItem) # 改变指定控件的位置(适用于水平布局)
elif type(self.layout) == QVBoxLayout:
self.VInsert(point,otherItem) # 改变指定控件的位置(适用于垂直布局)
# 鼠标拖拽接受事件
def dragEnterEvent(self,event):
event.setDropAction(Qt.MoveAction)
event.accept()
# 判定模式(横向判断)
def Hline(self,point,geometry,item):
x = geometry.x()
x2 = geometry.x()+geometry.width()
y = geometry.y()
if point.x() <= x+15: # 横向模式,鼠标处于控件左边
self.line.resize(5,item.height())
self.line.move(x,y) # 将原本设定好的标示线(QLabel)移动到指定位置
self.line.raise_() # 设置为最顶层
self.line.show() # 显示
elif x2-15 <= point.x(): # 横向模式,鼠标处于控件右边
self.line.resize(5,item.height())
self.line.move(x2-5,y)
self.line.raise_()
self.line.show()
else:
self.line.hide()
# 判定模式(竖向判断)
def Vline(self,point,geometry,item):
y = geometry.y()
y2 = geometry.y()+geometry.height()
x = geometry.x()
if point.y() <= y+15:
self.line.resize(item.width(),5)
self.line.move(x,y)
self.line.raise_()
self.line.show()
elif y2-15 <= point.y():
self.line.resize(item.width(),5)
self.line.move(x,y2-5)
self.line.raise_()
self.line.show()
else:
self.line.hide()
# 插入模式(横向判断)
def HInsert(self,point,otherItem):
geometry = otherItem.geometry()
x = geometry.x()
x2 = geometry.x()+geometry.width()
y = geometry.y()
oldIndex = int(self.drag.mimeData().text()) # 获得原先存储在mimeData中的索引信息
if oldIndex == self.layout.indexOf(otherItem):return # 如果otherItem和oldItem实属同一个控件,则不做改变
if point.x() <= x+15: # 插入在控件的左边
oldItem = self.layout.takeAt(oldIndex).widget()
index = self.layout.indexOf(otherItem) # 重新获得当前控件所处的布局中的索引位置
self.layout.insertWidget(index,oldItem)
elif x2-15 <= point.x(): # 插入在控件的右边
oldItem = self.layout.takeAt(oldIndex).widget()
index = self.layout.indexOf(otherItem) # 重新获得当前控件所处的布局中的索引位置
self.layout.insertWidget(index+1,oldItem)
else:
pass
self.line.hide()
# 插入模式(竖向判断)
def VInsert(self,point,otherItem):
geometry = otherItem.geometry()
y = geometry.y()
y2 = geometry.y()+geometry.height()
x = geometry.x()
oldIndex = int(self.drag.mimeData().text()) # 获得原先存储在mimeData中的索引信息
if oldIndex == self.layout.indexOf(otherItem):return # 如果otherItem和oldItem实属同一个控件,则不做改变
if point.y() <= y+15: # 插入在控件的上边
oldItem = self.layout.takeAt(oldIndex).widget()
index = self.layout.indexOf(otherItem) # 重新获得当前控件所处的布局中的索引位置
self.layout.insertWidget(index,oldItem)
elif y2-15 <= point.y(): # 插入在控件的下边
oldItem = self.layout.takeAt(oldIndex).widget()
index = self.layout.indexOf(otherItem) # 重新获得当前控件所处的布局中的索引位置
self.layout.insertWidget(index+1,oldItem)
else:
pass
self.line.hide()
# 指示条纹(实际是个QLabel)
class LineLable(QLabel):
def __init__(self,parent = None):
super(LineLable,self).__init__(parent)
self.setStyleSheet("background-color:red;")
self.hide() # 一开始隐藏
class mainWidget(QMainWindow):
def __init__(self,parent = None):
super(mainWidget,self).__init__(parent)
self.centerWidget = DragBaseLayout()
self.setCentralWidget(self.centerWidget)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = mainWidget()
win.show()
sys.exit(app.exec_())
4.结束语
本来想将网格布局(QGridLayout)也一并做成可拖拽移动式的布局,但是感觉有点麻烦,也快期末考试了,就没有继续做网格布局的了(后期做了再更新)。然后拖拽时感觉还是显得很突兀,要是有动画说不定会更好。然后之所以没有向前文所提的大佬一样做出控件拖拽时就脱出的效果,是因为自己做时遇到了脱出后原处仍会产生控件残影的情况一直解决不了。有大佬要是有其他更好的想法可以在下面留言,感谢!