You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PySide6 QTableWidget单元格部件拖拽问题:覆盖丢失与无法回位

QTableWidget单元格Widget拖拽交换修复方案

存在问题

当前拖拽实现有两个核心问题:

  • 拖拽到已有Widget的单元格时,原单元格Widget会被自动删除,无法保留或交换
  • 无法将Widget放回初始拖拽位置

修复后代码

from PySide6.QtWidgets import QApplication, QHBoxLayout, QWidget, QPushButton, QTableWidget
from PySide6.QtCore import Qt, QMimeData, QPoint
from PySide6.QtGui import QDrag


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() != Qt.LeftButton:
            return
        
        # 获取当前按钮所在的表格单元格位置
        table = self.parentWidget()
        if not isinstance(table, QTableWidget):
            return
        row = table.indexAt(self.pos()).row()
        col = table.indexAt(self.pos()).column()
        if row < 0 or col < 0:
            return

        # 构造拖拽数据,携带原单元格行列信息
        drag = QDrag(self)
        mime = QMimeData()
        mime.setData("text/row", str(row).encode())
        mime.setData("text/col", str(col).encode())
        drag.setMimeData(mime)
        drag.exec(Qt.MoveAction)


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)
        self.nb_col = 5
        self.nb_row = 5
        self.tbl = QTableWidget(self.nb_row, self.nb_col)
        self.tbl.setHorizontalHeaderLabels([str(x) for x in range(self.nb_col)])
        self.tbl.setVerticalHeaderLabels([str(x) for x in range(self.nb_row)])
        # 表格viewport需要接受拖拽
        self.tbl.viewport().setAcceptDrops(True)

        self.layout = QHBoxLayout()
        self.layout.addWidget(self.tbl)

        nom = ['A', 'B', 'C', 'D']
        for l in range(len(nom)):
            new_btn = DragButton(nom[l])
            new_btn2 = DragButton(nom[l] + 'j')
            self.tbl.setCellWidget(0, l, new_btn)
            self.tbl.setCellWidget(l, 1, new_btn2)
        self.setLayout(self.layout)

    def dragEnterEvent(self, e):
        # 只接受DragButton的拖拽
        if isinstance(e.source(), DragButton):
            e.acceptProposedAction()

    def dropEvent(self, e):
        # 获取拖拽携带的原单元格信息
        mime = e.mimeData()
        if not (mime.hasFormat("text/row") and mime.hasFormat("text/col")):
            return
        
        original_row = int(mime.data("text/row").data().decode())
        original_col = int(mime.data("text/col").data().decode())
        dragged_widget = e.source()

        # 计算目标单元格位置(基于viewport的坐标)
        view_pos = self.tbl.viewport().mapFromGlobal(e.globalPosition().toPoint())
        target_row = self.tbl.rowAt(view_pos.y())
        target_col = self.tbl.columnAt(view_pos.x())

        # 校验目标位置合法性
        if target_row < 0 or target_col < 0 or target_row >= self.nb_row or target_col >= self.nb_col:
            return
        
        # 如果放回原位置,直接结束
        if original_row == target_row and original_col == target_col:
            e.acceptProposedAction()
            return

        # 获取目标单元格的现有Widget
        target_widget = self.tbl.cellWidget(target_row, target_col)

        # 交换两个单元格的Widget
        self.tbl.setCellWidget(original_row, original_col, target_widget)
        self.tbl.setCellWidget(target_row, target_col, dragged_widget)

        e.acceptProposedAction()


if __name__ == "__main__":
    app = QApplication([])
    w = Window()
    w.showMaximized()
    app.exec()

关键修复点

  • 拖拽数据携带位置信息:在DragButton的拖拽事件中,获取当前单元格的行列并存入MimeData,确保drop时能定位原位置
  • 坐标计算修正:使用表格的viewport转换全局坐标,避免header区域干扰行列计算
  • Widget交换逻辑:不再直接覆盖目标单元格Widget,而是将原位置和目标位置的Widget互相交换,保留所有组件
  • 合法性校验:仅接受DragButton类型的拖拽源,同时校验目标行列的有效性
  • 支持原位置放回:判断目标位置与原位置相同时不执行交换,允许将Widget拖回原位

内容的提问来源于stack exchange,提问作者CSonneck

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 20:40:55