下面我有一个可用的拖放示例,用于使用 PyQt5 对 qtableview 中相同列长度的行进行重新排序(借助此处的StackOverflow 问题)。但是,我希望在 qtableview 表中执行相同的操作,其中一行或两行具有跨越总列数的合并单元格(如下图的第二行)。
最好的解决方法是什么?我是否应该在拖放点处删除合并 (clearSpans),然后根据单元格值重新合并(尽管我尝试这样做时没有成功),或者是否有办法在单元格合并完好的情况下进行拖放重新排序?
这是适用于相等列的行数据的代码,但在合并行时会失败
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: if (self.index(index.row(), 1).data()) == 'abc': return QBrush(Qt.yellow) if (self.index(index.row(), 1).data()) == 'def': return QBrush(Qt.blue) if (self.index(index.row(), 1).data()) == 'ghi': return QBrush(Qt.magenta) if role in (Qt.DisplayRole, Qt.EditRole): return self._data[index.row()][index.column()] 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 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() #self.clearSpans() 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) event.accept() super().dropEvent(event) class sample_data(QMainWindow): def __init__(self): super().__init__() tv = myTableView(self) tv.setModel(myModel([ ["first", 'abc', 123], ["second"], ["third", 'def', 456], ["fourth", 'ghi', 789], ])) self.setCentralWidget(tv) tv.setSpan(1, 0, 1, 3) self.show() if __name__ == '__main__': app = QApplication([]) test = sample_data() raise SystemExit(app.exec_())
垂直标题的各部分可以设为可移动的,因此无需自己实现此功能。这显然意味着垂直标题将可见,但可以通过将各部分设为空白来缓解这种情况,这将导致标题相对较窄:
请注意,移动部分(而不是行)纯粹是视觉上的- 底层模型永远不会被修改。不过,这在实践中并不重要,因为标题提供了从逻辑索引转换为视觉索引的方法。它确实带来了一些额外的好处 - 例如,很容易返回到以前的状态(即通过使用标题的saveState和restoreState方法)。
Alt+Up以下是基于您的示例的工作演示。可以通过拖放节标题或在选择行时按/来重新排序行Alt+Down。可以通过按 切换垂直标题F6。可以通过按 打印逻辑F7行。
更新:
我还添加了通过拖放行本身来移动部分的支持。
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] else: return '' 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: if (self.index(index.row(), 1).data()) == 'abc': return QBrush(Qt.yellow) if (self.index(index.row(), 1).data()) == 'def': return QBrush(Qt.blue) if (self.index(index.row(), 1).data()) == 'ghi': return QBrush(Qt.magenta) if role in (Qt.DisplayRole, Qt.EditRole): return self._data[index.row()][index.column()] 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 class myTableView(QTableView): def __init__(self, parent): super().__init__(parent) header = self.verticalHeader() header.setSectionsMovable(True) header.setSectionResizeMode(QHeaderView.Fixed) header.setFixedWidth(10) QShortcut('F7', self, self.getLogicalRows) QShortcut('F6', self, self.toggleVerticalHeader) QShortcut('Alt+Up', self, lambda: self.moveRow(True)) QShortcut('Alt+Down', self, lambda: self.moveRow(False)) self.setSelectionBehavior(self.SelectRows) self.setSelectionMode(self.SingleSelection) self.setDragDropMode(self.InternalMove) self.setDragDropOverwriteMode(False) 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): header = self.verticalHeader() from_index = header.visualIndex(from_index) to_index = header.visualIndex(to_index) header.moveSection(from_index, to_index) event.accept() super().dropEvent(event) def toggleVerticalHeader(self): self.verticalHeader().setHidden(self.verticalHeader().isVisible()) def moveRow(self, up=True): selection = self.selectedIndexes() if selection: header = self.verticalHeader() row = header.visualIndex(selection[0].row()) if up and row > 0: header.moveSection(row, row - 1) elif not up and row < header.count() - 1: header.moveSection(row, row + 1) def getLogicalRows(self): header = self.verticalHeader() for vrow in range(header.count()): lrow = header.logicalIndex(vrow) index = self.model().index(lrow, 0) print(index.data()) class sample_data(QMainWindow): def __init__(self): super().__init__() tv = myTableView(self) tv.setModel(myModel([ ["first", 'abc', 123], ["second"], ["third", 'def', 456], ["fourth", 'ghi', 789], ])) self.setCentralWidget(tv) tv.setSpan(1, 0, 1, 3) if __name__ == '__main__': app = QApplication(['Test']) test = sample_data() test.setGeometry(600, 100, 350, 185) test.show() app.exec_()