重新排序包含跨列的 QTableView 行

Re-ordering QTableView rows that include spanned columns

我在下面有一个有效的拖放示例,用于使用 PyQt5 为 qtable 视图重新排序相同列长度的行(在这个 Whosebug 问题 的帮助下)。但是,我希望在 qtableview table 上执行相同的操作,其中一两行合并了跨越总列数的单元格(如下图中的第二行)。

最好的解决方法是什么?我是否应该在 drag/drop 点删除合并 (clearSpans),然后根据单元格值进行重新合并(尽管当我尝试这样做时它不起作用),或者有没有办法 drag/drop重新排序并完整合并单元格?

以下代码适用于等列的行数据,但在合并行时失败

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_())

垂直headercan be made movable的部分,所以没有必要自己实现这个功能。这显然意味着垂直 header 将可见,但可以通过将部分设为空白来缓解这种情况,这将导致相对较窄的 header:

请注意,移动部分(而不是行)纯粹是 视觉 - 基础模型永远不会被修改。不过,这在实践中并不重要,因为 header 提供了从 logical to visual indices. And it does bring some additional benefits - for example, it's very easy to return to a previous state (i.e. by using the header's saveState and restoreState 方法转换而来的方法。

下面是一个基于您的示例的工作演示。可以通过拖放 header 部分或按 Alt+Up / Alt+Down[=34] 来 re-ordered 行=] 当一行被选中时。垂直 header 可以通过按 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_()