PyQt5在QTableview拖拽导致选中行消失

PyQt5 dragging and dropping in QTableview causes selected row to disappear

继上一个问题 之后,我希望通过将 removeRows 函数添加到我的 PyQt5 中,将按键删除行功能添加到我的 qtableview table模型。但是,自从添加此功能后,它扰乱了我的拖放功能,因此当拖放到 qtable 视图 table 中的其他位置时,拖动的行会消失。 有什么方法可以防止拖拽的行消失吗?

NB: Interestingly, when selecting the vertical header 'column' the drag/drop functionality works, but I'm keen to find a solution for dragging and dropping on row selection.

下面是我的代码,在模型中添加了 removeRows 函数,在视图中添加了 keyPressEvent

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 removeRows(self, position, rows, QModelIndex):
        self.beginRemoveRows(QModelIndex, position, position + rows - 1)
        for i in range(rows):
            del (self._data[position])
        self.endRemoveRows()
        self.layoutChanged.emit()
        return True

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

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Delete:
            index = self.currentIndex()
            try:
                self.model().removeRows(index.row(), 1, index)
            except IndexError:
                pass
        else:
            super().keyPressEvent(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)

if __name__ == '__main__':

    app = QApplication(['Test'])
    test = sample_data()
    test.setGeometry(600, 100, 350, 185)
    test.show()
    app.exec_()

主要的“问题”是,默认情况下,QAbstractItemModel 的 removeRows 什么都不做(returns False)。

技术问题有点微妙。
项视图中的拖动操作始终以 startDrag() 开头,它创建一个 QDrag object 并调用它的 exec()。当用户删除数据时,只要接受的删除操作是 MoveAction,该实现也会调用私有 clearOrRemove 函数,最终覆盖数据 删除行(s).

您已经使用了setDragDropOverwriteMode(False),所以它会调用removeRows。您之前的代码曾经有效,因为如前所述,默认实现什么都不做,但现在您重新实现了它,并且在这种情况下它实际上删除了行。

解决方案是在放置事件为移动时更改放置动作(这有点不直观,但由于操作已经执行,这应该不是问题)。使用 IgnoreAction 将避免不需要的行为,因为在这种情况下 clearOrRemove 将不再被调用:

    def dropEvent(self, event):
        # ...
        if (0 <= from_index < self.model().rowCount() and
            0 <= to_index < self.model().rowCount() and
            from_index != to_index):
            # ...
            header.moveSection(from_index, to_index)
            event.accept()
            event.setDropAction(Qt.IgnoreAction)
        super().dropEvent(event)

更新

当项目没有填满整个视口时,放置事件可能发生在项目范围之外,导致来自 indexAt() 的无效 QModelIndex 不仅在放置超出最后一行时,而且在最后一列之外.
由于您只对垂直移动感兴趣,因此解决方案是从垂直 header 获取 to_index,并最终在它仍然无效时将其设置为最后一行 (-1):

    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

        globalPos = self.viewport().mapToGlobal(event.pos())
        header = self.verticalHeader()
        to_index = header.logicalIndexAt(header.mapFromGlobal(globalPos).y())
        if to_index < 0:
            to_index = header.logicalIndex(self.model().rowCount() - 1)

        if from_index != to_index:
            from_index = header.visualIndex(from_index)
            to_index = header.visualIndex(to_index)
            header.moveSection(from_index, to_index)
            event.accept()
            event.setDropAction(Qt.IgnoreAction)
        super().dropEvent(event)