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

PyQt5中QAbstractTableModel整行拖拽功能实现求助

实现QAbstractTableModel的整行拖拽功能

你已经搭好了基础框架,设置了表格的选择行为和拖拽模式,现在只需要把MIME相关的三个函数改对就行——默认的MIME处理是针对单个单元格的,完全不适合整行移动的场景。下面是修改后的完整代码,我会一步步给你解释关键改动:

关键修改拆解

1. 自定义MIME类型

首先得定义一个专属的MIME类型,用来区分我们传递的整行数据和默认的单元格数据:

def mimeTypes(self):
    # 用自定义标识来标记整行数据
    return ['application/vnd.tableview.rowdata']

2. 打包整行拖拽的数据(mimeData)

当拖拽开始时,我们需要把选中行的所有数据序列化后打包,这里用Qt的QDataStream来处理,确保数据在组件间传递的正确性:

def mimeData(self, indexes):
    mime_data = QtCore.QMimeData()
    if not indexes:
        return mime_data
    
    # 因为设置了单行选中,直接取第一个选中项的行索引
    original_row = indexes[0].row()
    row_data = self.materials[original_row]
    
    # 序列化数据:先写列数,再逐列存数据,最后存原行索引(方便后续定位要删除的行)
    byte_array = QtCore.QByteArray()
    stream = QtCore.QDataStream(byte_array, QtCore.QIODevice.WriteOnly)
    stream.writeInt32(len(row_data))
    for item in row_data:
        stream.writeQVariant(item)
    stream.writeInt32(original_row)
    
    mime_data.setData('application/vnd.tableview.rowdata', byte_array)
    return mime_data

3. 解析并处理拖拽数据(dropMimeData)

拖拽完成时,我们要解析传递过来的数据,把行移动到目标位置,还要处理移动动作对应的原行删除:

def dropMimeData(self, data, action, row, col, parent):
    if action == QtCore.Qt.IgnoreAction:
        return True
    
    # 先检查是不是我们自定义的MIME类型
    if not data.hasFormat('application/vnd.tableview.rowdata'):
        return False
    
    # 解析序列化的数据
    byte_array = data.data('application/vnd.tableview.rowdata')
    stream = QtCore.QDataStream(byte_array, QtCore.QIODevice.ReadOnly)
    
    col_count = stream.readInt32()
    row_data = []
    for _ in range(col_count):
        row_data.append(stream.readQVariant())
    original_row = stream.readInt32()
    
    # 确定插入位置:如果row是-1,说明插入到parent行的位置;否则插入到指定row处
    insert_row = row if row != -1 else parent.row()
    # 处理拖拽到表格末尾的情况
    if insert_row < 0 or insert_row > self.rowCount(QtCore.QModelIndex()):
        insert_row = self.rowCount(QtCore.QModelIndex())
    
    # 避免把行插入到原位置
    if original_row == insert_row:
        return True
    
    # 插入新行并填充数据
    self.insertRows(insert_row, 1)
    for col_idx, value in enumerate(row_data):
        idx = self.index(insert_row, col_idx)
        self.setData(idx, value, QtCore.Qt.EditRole)
    
    # 如果是移动动作,删除原行(注意:如果原行在插入行之后,插入后原行索引会+1,要调整)
    if action == QtCore.Qt.MoveAction:
        if original_row > insert_row:
            original_row += 1
        self.removeRows(original_row, 1)
    
    return True

完整修改后的代码

下面是整合了所有改动的可运行代码:

from PyQt5 import QtCore, QtGui, QtWidgets
import sys

class Mainwindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.table = QtWidgets.QTableView()
        self.setCentralWidget(self.table)
        headers = [None, 'Matreial', 'γ, kg/m3', 'λa', 'λb']
        data = [
            [1, 'Blocks γ=500 GOST 31359-2007', 500, 0.18, 0.22],
            [2, 'Blocks γ=600 GOST 31359-2008', 600, 0.25, 0.27],
            [3, 'Insulation', '80-125', 0.041, 0.042]
        ]
        model = Materials(data, headers)
        self.table.setModel(model)
        self.table.setSelectionBehavior(self.table.SelectRows)
        self.table.setSelectionMode(self.table.SingleSelection)
        self.table.setDragDropMode(self.table.InternalMove)
        self.table.setDropIndicatorShown(True)
        self.table.setDragEnabled(True)
        self.table.setAcceptDrops(True)

class Materials(QtCore.QAbstractTableModel):
    def __init__(self, materials = [[]], headers = [], parent = None):
        super(Materials, self).__init__()
        self.materials = materials
        self.headers = headers

    def rowCount(self, parent):
        return len(self.materials)

    def columnCount(self, parent):
        return len(self.headers)

    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            column = index.column()
            value = self.materials[row][column]
            return value

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            text = str(value)
            if type(text)==str:
                self.materials[row][column] = text
                self.dataChanged.emit(index, index)
                return True
        return False

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self.headers[section]

    def flags(self, index):
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled

    def insertRows(self, position, rows, parent = QtCore.QModelIndex()):
        self.beginInsertRows(parent, position, position + rows - 1)
        for i in range(rows):
            defaultValues = ['' for i in range(self.columnCount(None))]
            self.materials.insert(position, defaultValues)
        self.endInsertRows()
        return True

    def removeRows(self, position, rows, parent = QtCore.QModelIndex()):
        self.beginRemoveRows(parent, position, position + rows - 1)
        for i in range(rows):
            value = self.materials[position]
            self.materials.remove(value)
        self.endRemoveRows()
        return True

    def supportedDropActions(self):
        return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction

    def supportedDragActions(self):
        return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction

    def mimeTypes(self):
        return ['application/vnd.tableview.rowdata']

    def mimeData(self, indexes):
        mime_data = QtCore.QMimeData()
        if not indexes:
            return mime_data
        
        original_row = indexes[0].row()
        row_data = self.materials[original_row]
        
        byte_array = QtCore.QByteArray()
        stream = QtCore.QDataStream(byte_array, QtCore.QIODevice.WriteOnly)
        stream.writeInt32(len(row_data))
        for item in row_data:
            stream.writeQVariant(item)
        stream.writeInt32(original_row)
        
        mime_data.setData('application/vnd.tableview.rowdata', byte_array)
        return mime_data

    def dropMimeData(self, data, action, row, col, parent):
        if action == QtCore.Qt.IgnoreAction:
            return True
        
        if not data.hasFormat('application/vnd.tableview.rowdata'):
            return False
        
        byte_array = data.data('application/vnd.tableview.rowdata')
        stream = QtCore.QDataStream(byte_array, QtCore.QIODevice.ReadOnly)
        
        col_count = stream.readInt32()
        row_data = []
        for _ in range(col_count):
            row_data.append(stream.readQVariant())
        original_row = stream.readInt32()
        
        insert_row = row if row != -1 else parent.row()
        if insert_row < 0 or insert_row > self.rowCount(QtCore.QModelIndex()):
            insert_row = self.rowCount(QtCore.QModelIndex())
        
        if original_row == insert_row:
            return True
        
        self.insertRows(insert_row, 1)
        for col_idx, value in enumerate(row_data):
            idx = self.index(insert_row, col_idx)
            self.setData(idx, value, QtCore.Qt.EditRole)
        
        if action == QtCore.Qt.MoveAction:
            if original_row > insert_row:
                original_row += 1
            self.removeRows(original_row, 1)
        
        return True

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    application = Mainwindow()
    application.show()
    sys.exit(app.exec())

额外说明

  • 我在mimeData里加入了原行索引的传递,这样就不用依赖表格的选中状态来定位要删除的行,避免拖拽过程中选中状态意外变化的问题。
  • dropMimeData里处理了拖拽到表格末尾的边界情况,确保行能正确插入。
  • 如果你需要支持多行拖拽,只需要修改mimeData来批量打包多行数据,再在dropMimeData里批量插入和删除就行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 19:33:11