Home > Blockchain >  PyQt5 dragging and dropping in QTableview causes selected row to disappear
PyQt5 dragging and dropping in QTableview causes selected row to disappear

Time:10-28

Following on from a previous question here, I looked to add delete row on key press functionality to my qtableview table in PyQt5 by adding the removeRows function to my model. However, since adding this function it has disrupted my drag and drop functionality, whereby the dragged row disappears when dropping elsewhere in the qtableview table. Is there anyway I can prevent the dragged row from disappearing?

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.

Here's my code below with the added removeRows function in the model, and also the keyPressEvent in the view

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

CodePudding user response:

The main "problem" is that, by default, removeRows of QAbstractItemModel doesn't do anything (and returns False).

The technical problem is a bit more subtle.
A drag operation in an item view always begins with startDrag(), which creates a QDrag object and calls its exec(). When the user drops the data, that implementation also calls a private clearOrRemove function whenever the accepted drop action is MoveAction, which eventually overwrites the data or removes the row(s).

You've used setDragDropOverwriteMode(False), so it will call removeRows. Your previous code used to work because, as said, the default implementation does nothing, but now you've reimplemented it, and it actually deletes rows in that case.

The solution is to change the drop action whenever the drop event is a move (which is a bit unintuitive, but since the operation has been already performed, that shouldn't be an issue). Using IgnoreAction will avoid the unwanted behavior, as that clearOrRemove won't be called anymore in that case:

    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)
  • Related