PyQt实现表格和树控件间的拖拽功能

526 阅读4分钟

1.起因

最近在开发软件时,需要用户根据表格中的数据,将其组合成一个树状结构。思来想去,就觉得只有拖拽操作是最直观和优雅的。所以花了点时间去研究了下pyqt5中的拖拽功能。

2.理论

pyqt5中拖拽操作存在两个动作:拖动(drag)和放置(drop)。
简略流程:

image.png

①拖动

要想将列表中数据拖动,先调用方法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.成果

下面就是基础成果:

QQ20231226-19553-HD.gif

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)