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
相关产品推荐
相关产品推荐

