1.起因
最近在开发软件时,需要用户根据表格中的数据,将其组合成一个树状结构。思来想去,就觉得只有拖拽操作是最直观和优雅的。所以花了点时间去研究了下pyqt5中的拖拽功能。
2.理论
pyqt5中拖拽操作存在两个动作:拖动(drag)和放置(drop)。
简略流程:
①拖动
要想将列表中数据拖动,先调用方法setDragEnabled(True),允许拖动列表中的数据。
setDragEnabled(True) # 允许拖拽
在拖动时就要考虑将要移动的数据打包,这里重写mousePressEvent(event)方法,在点击左键时就将数据打包。为什么不在mouseMoveEvent(event)里打包数据,因为和我用的三方冲突了🤣。
def mousePressEvent(self, event):
# 判断点击的是否是左键
if event.button() != Qt.LeftButton:
return
# 判断点击时是否存在数据
itemIndex = self.indexAt(event.pos())
if not itemIndex.isValid():
return
# 取出一行的全部数据,用dataList记录
row: int = itemIndex.row()
columnCount = self.columnCount()
dataList = []
for column in range(columnCount):
item = self.item(row, column)
if item is not None:
dataList.append(item.text())
print(f"这列的数据{str(dataList)}")
# 打包数据
byteArray = QByteArray(str(dataList).encode()) # QMimeData不支持list
mimeData = QMimeData()
# 设置列表数据结构
mimeData.setData("application/x-qt-listdata", byteArray)
# 数据打包上车
drag = QDrag(self)
drag.setMimeData(mimeData)
drag.exec(Qt.MoveAction | Qt.CopyAction)
super(TableWidgetDropDrag, self).mousePressEvent(event)
拖放通过调用 QDrag.exec()函数而启动,该函数是一个阻塞函数(但不会阻塞主事件循环),这意味着在拖放操作结束(松开按键)之前,不会返回该函数。
将数据从列表拖动到树时会触发dragEnterEvent(event),使用accept()将接受此拖动,而使用ignore()将忽略此次拖动。dragMoveEvent()也是同理。
def dragEnterEvent(self, event):
# 判断拖拽的内容中有没有咱们需要的数据
if event.mimeData().hasFormat("application/x-qt-listdata"):
logger.info("dragEnterEvent允许拖拽")
event.accept() # 接受拖动操作
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/x-qt-listdata"):
event.accept() # 接受拖动操作
else:
event.ignore()
②放置
这是拖拽的最后一步,也就是拖动结束(鼠标抬起时),需要按照要求将event.mimeData()的数据添加到目标控件(TreeWidget)中。
首先允许树控件能被拖入数据:
self.setAcceptDrops(True)
self.setDragEnabled(True)
再重写dropEvent,约束数据放下时的行为:
def dropEvent(self, event) -> None:
source = event.source()
if source == self:
# 控件内部拖拽, 不处理
print("拖拽事件发生于控件内部,不处理")
return
# 来自外部的拖拽
mineData = event.mimeData()
if mineData.hasFormat("application/x-qt-listdata"):
# 数据处理:将mineData转换成list
data = mineData.data("application/x-qt-listdata")
byteArray = bytearray(data)
dataList = eval(byteArray.decode())
print(f"拖入的数据为:{dataList}")
pos = event.pos()
targetItem = self.itemAt(pos) # 拖拽最后的鼠标位置的item
item = QTreeWidgetItem(dataList)
if targetItem is None:
# 空白区域
self.addTopLevelItem(item)
else:
# 节点
targetItem.addChild(item)
if targetItem.isExpanded() is False:
targetItem.setExpanded(True) # 展开节点
else:
print("没有数据")
3.成果
下面就是基础成果:
4.源代码
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class TableWidgetDrag(QTableWidget):
def __init__(self, parent=None):
super(TableWidgetDrag, self).__init__(parent=parent)
self.setDragEnabled(True) # 允许拖拽
def mousePressEvent(self, event):
# Only proceed if the click is with the left mouse button
if event.button() != Qt.LeftButton:
return
# Convert the mouse event position to a table item index
itemIndex = self.indexAt(event.pos())
if not itemIndex.isValid():
return
row: int = itemIndex.row()
columnCount = self.columnCount()
dataList = []
for column in range(columnCount):
item = self.item(row, column)
if item is not None:
dataList.append(item.text())
print(f"这列的数据{str(dataList)}")
byteArray = QByteArray(str(dataList).encode())
mimeData = QMimeData()
# 设置列表数据结构
mimeData.setData("application/x-qt-listdata", byteArray)
drag = QDrag(self)
drag.setMimeData(mimeData) # 数据打包上车
drag.exec(Qt.MoveAction | Qt.CopyAction)
super(TableWidgetDrag, self).mousePressEvent(event)
class TreeWidgetDrop(QTreeWidget):
def __init__(self, parent=None):
super(TreeWidgetDrop, self).__init__(parent=parent)
self.setAcceptDrops(True) # 允许数据拖入
self.setDragEnabled(True) # 允许拖拽
def dropEvent(self, event) -> None:
source = event.source()
if source == self:
# 控件内部拖拽
print("拖拽事件发生于控件内部,不处理")
return
# 来自外部的拖拽
mineData = event.mimeData()
if mineData.hasFormat("application/x-qt-listdata"):
# 数据处理:将mineData转换成list
data = mineData.data("application/x-qt-listdata")
byteArray = bytearray(data)
dataList = eval(byteArray.decode())
print(f"拖入的数据为:{dataList}")
pos = event.pos()
targetItem = self.itemAt(pos) # 拖拽最后的鼠标位置的item
item = QTreeWidgetItem(dataList)
if targetItem is None:
# 空白区域
self.addTopLevelItem(item)
else:
# 节点
targetItem.addChild(item)
if targetItem.isExpanded() is False:
targetItem.setExpanded(True) # 展开节点
else:
print("没有数据")
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasFormat("application/x-qt-listdata"):
event.accept() # 接受拖拽操作
else:
event.ignore()
def dragMoveEvent(self, event: QDragMoveEvent):
if event.mimeData().hasFormat("application/x-qt-listdata"):
event.accept() # 接受拖拽操作
else:
event.ignore()
class TestWindow(QWidget):
def __init__(self):
super(TestWindow, self).__init__()
self.table = TableWidgetDrag()
self.tree = TreeWidgetDrop()
self.initContrllers()
self.initLayout()
def initContrllers(self) -> None:
# init tree
self.tree.setMinimumSize(200, 200)
self.tree.setHeaderLabels(["序号", "型号", "数据"])
# init table
self.table.setMinimumSize(200, 200)
self.table.setRowCount(2)
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["序号", "型号", "数据"])
self.table.verticalHeader().hide()
data = [
["01", "BX-10", "55"],
["02", "BX-9", "45"]
]
for i, line in enumerate(data):
for j, item in enumerate(line):
cellText = data[i][j]
cell = QTableWidgetItem(cellText)
cell.setTextAlignment(Qt.AlignCenter)
self.table.setItem(i, j, cell)
def initLayout(self) -> None:
self.resize(900, 300)
hBox = QHBoxLayout(self)
hBox.addWidget(self.table)
hBox.addWidget(self.tree)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = TestWindow()
win.show()
sys.exit(app.exec_())
5.文章参考
① Qt拖放(1):拖放基本原理(QDrag类)-CSDN博客
② QTreeView的拖拽 - 知乎 (zhihu.com)