重新排序包含跨列的 QTableView 行

Posted

技术标签:

【中文标题】重新排序包含跨列的 QTableView 行【英文标题】:Re-ordering QTableView rows that include spanned columns 【发布时间】:2021-11-01 15:37:51 【问题描述】:

我在下面有一个有效的拖放示例,用于使用 PyQt5 为 qtableview 重新排序具有相同列长度的行(在此 *** 问题here 的帮助下)。但是,我希望在 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_())

【问题讨论】:

垂直表头can be made movable的部分,所以不需要自己实现这个功能。这显然意味着垂直标题将可见,但这可以通过将部分设为空白来缓解,这将导致标题相对较窄。您还可以添加键盘快捷键来切换垂直标题的可见性。 @ekhumoro 这只是将 self.verticalHeader().hide() 替换为 self.verticalHeader().setSectionsMovable(True) 的情况吗?虽然我试过了,但垂直标题仍然隐藏。 不,还有更多。如果您有兴趣,我可以提供一个工作示例的答案。我可能应该提到的一件事是移动部分纯粹是视觉 - 底层模型永远不会被修改。不过,这在实践中并不重要,因为标题提供了从逻辑索引转换为视觉索引的方法。而且它确实带来了一些额外的好处——例如,恢复原始状态非常容易。 我对一个工作示例非常感兴趣,非常感谢。我只是对模型/视图设置有所了解。 好的 - 我已将我的 cmets 转换为答案并包含一个工作演示。 【参考方案1】:

垂直标题can be made movable的部分,因此无需自己实现此功能。这显然意味着垂直标题将是可见的,但这可以通过将部分设为空白来缓解,这将导致标题相对较窄:

请注意,移动部分(而不是行)纯粹是视觉 - 永远不会修改底层模型。不过,这在实践中并不重要,因为标头提供了从logical 转换为visual 索引的方法。而且它确实带来了一些额外的好处——例如,很容易返回到以前的状态(即通过使用标头的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_()

【讨论】:

感谢您的帮助@ekhumoro,您的示例在从垂直标题侧边栏中拖放时有效,有没有办法通过选择您知道的行本身来拖放?不管怎样,你让我向前迈了一步,谢谢。 @MikG 我通过调整您的拖放事件处理程序添加了拖放支持,以便它移动部分。请参阅我的更新答案。 谢谢,这正是我想要实现的。我会花一些时间学习你写的东西,谢谢你的帮助。 嗨@ekhumoro,在重新排序行时,我注意到我的self._data 排序没有反映移动行的视觉变化。因此,如果我将最后一行移动到第二行,当我遍历每一行(例如 self._data 中的行)时,它会跳过第二行,只在最后执行可视的第二行(之前是最后一行) )。在 self._data 中反映拖放的最佳方式是什么? @MikG 正如我在回答中所说,底层模型从未被修改过——运动纯粹是视觉。因此,您只需在 logical 和 visual 索引之间进行转换即可获得所需的行。这是设计中经过深思熟虑的部分,因为它允许恢复原始状态。

以上是关于重新排序包含跨列的 QTableView 行的主要内容,如果未能解决你的问题,请参考以下文章

qtabelwidget怎么得到指定行和列的值

pyqt qt4 QTableView 如何禁用某些列的排序?

QTableView 将视图重新聚焦到特定列

QSqlQueryModel 配合Qtableview的表格,怎么实现点击头部排序

QTableView“ResizeToContents”列太宽,启用排序

HTML中 table 中的跨行跨列怎么拼写?