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

PyQt5 QTableView含跨列合并单元格的行拖拽重排方案咨询

实现方案

核心问题原因

你当前代码失效的核心原因是:单元格合并(setSpan)的配置是存储在QTableView视图层,而非绑定到数据模型,你只移动了模型中的数据,视图层的合并配置没有对应更新,自然出现显示异常。

两种可行实现路径

路径1:移动行后重建合并规则(最简单,适合合并规则固定的场景)

你之前的思路是可行的,只需要补全「明确的合并判断规则」+「移动后重建合并」的逻辑即可,不需要在拖拽开始清span,直接移动完覆盖重建所有span就行。

路径2:拖拽过程中同步span位置(适合合并规则不固定、用户可自定义合并的场景)

拖拽前先记录所有合并单元格的位置属性,行移动后计算每个合并单元格的新行号,删除旧span后在新位置重新设置相同属性的span,可保留原有合并状态不需要重建。

完整可运行修改代码

这里用路径1做示例,适配你给出的测试场景:

from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex

class myModel(QAbstractTableModel):

    def __init__(self, data, parent=None, *args):
        super().__init__(parent, *args)
        self._data = data or []
        self._headers = ['Type', 'result', 'count']

    def rowCount(self, index=None):
        return len(self._data)

    def columnCount(self, index=None):
        return len(self._headers)

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                if section < 0 or section >= len(self._headers):
                    return ""
                else:
                    return self._headers[section]
        return None

    def data(self, index, role=None):

        if role == Qt.TextAlignmentRole:
            return Qt.AlignHCenter

        if role == Qt.ForegroundRole:
            return QBrush(Qt.black)
        if role == Qt.BackgroundRole:
            if (self.index(index.row(), 0).data().startswith('second')):
                return QBrush(Qt.green)
            else:
                col1_data = self.index(index.row(), 1).data()
                if col1_data == 'abc':
                    return QBrush(Qt.yellow)
                if col1_data == 'def':
                    return QBrush(Qt.blue)
                if col1_data == 'ghi':
                    return QBrush(Qt.magenta)

        if role in (Qt.DisplayRole, Qt.EditRole):
            # 防止跨行合并的行取列数据报错
            if index.column() < len(self._data[index.row()]):
                return self._data[index.row()][index.column()]
            return ""
        
    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled

    def supportedDropActions(self) -> bool:
        return Qt.MoveAction | Qt.CopyAction

    def relocateRow(self, row_source, row_target) -> None:
        row_a, row_b = max(row_source, row_target), min(row_source, row_target)
        self.beginMoveRows(QModelIndex(), row_a, row_a, QModelIndex(), row_b)
        self._data.insert(row_target, self._data.pop(row_source))
        self.endMoveRows()


class myTableView(QTableView):

    def __init__(self, parent):
        super().__init__(parent)
        self.verticalHeader().hide()
        self.setSelectionBehavior(self.SelectRows)
        self.setSelectionMode(self.SingleSelection)
        self.setDragDropMode(self.InternalMove)
        self.setDragDropOverwriteMode(False)

    # 新增:根据数据规则重建所有合并单元格
    def rebuild_spans(self):
        self.clearSpans()
        col_count = self.model().columnCount()
        for row in range(self.model().rowCount()):
            # 规则:行数据长度为1时,跨全部列合并
            row_data = self.model()._data[row]
            if len(row_data) == 1:
                self.setSpan(row, 0, 1, col_count)

    def dropEvent(self, event):
        if (event.source() is not self or
            (event.dropAction() != Qt.MoveAction and
             self.dragDropMode() != QAbstractItemView.InternalMove)):
            super().dropEvent(event)

        selection = self.selectedIndexes()
        from_index = selection[0].row() if selection else -1
        to_index = self.indexAt(event.pos()).row()
        if (0 <= from_index < self.model().rowCount() and
            0 <= to_index < self.model().rowCount() and
            from_index != to_index):
            self.model().relocateRow(from_index, to_index)
            # 移动完成后重建合并单元格
            self.rebuild_spans()
            event.accept()
        super().dropEvent(event)


class sample_data(QMainWindow):
    def __init__(self):
        super().__init__()
        tv = myTableView(self)
        model = myModel([
            ["first", 'abc', 123],
            ["second"],
            ["third", 'def', 456],
            ["fourth", 'ghi', 789],
        ])
        tv.setModel(model)
        self.setCentralWidget(tv)
        # 初始化时调用重建方法,不需要手动setSpan
        tv.rebuild_spans()

        self.show()


if __name__ == '__main__':
    app = QApplication([])
    test = sample_data()
    raise SystemExit(app.exec_())

代码修改说明

  • 补全了data方法的边界判断,避免合并行取超出数据长度的列时报错
  • 新增rebuild_spans方法,统一管理合并规则,初始化和移动行后都调用这个方法生成合并配置
  • 去掉了初始化时手动调用setSpan的逻辑,统一用规则生成,避免和重建逻辑冲突

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 08:09:04