Search code examples
pythonpyqtqtableviewqabstracttablemodel

How can I move a row in a QTableView (QAbstractTableModel) using beginMoveRows?


I am trying to move rows in my QTableView in the example below, but am struggling to understand how to correctly call beginMoveRows. My example has 3 buttons to do various row movements, and the 3rd one (move a row down by 1) causes my program to crash.

I think it crashes because of this statement in the docs...

Note that if sourceParent and destinationParent are the same, you must ensure that the destinationChild is not within the range of sourceFirst and sourceLast + 1. You must also ensure that you do not attempt to move a row to one of its own children or ancestors. This method returns false if either condition is true, in which case you should abort your move operation.

But doesn't this restriction mean I am not able to move a row down by 1? In my case sourceLast==sourceFirst since I am just moving a single row at a time so this statement basically says my destination index cannot be equal to my source index + 1, which is the definition of moving a row down by 1. Am I misunderstanding what these arguments mean? How can I move a row down 1 position in this example?

from PyQt5 import QtWidgets, QtCore, QtGui
import sys

from PyQt5.QtCore import QModelIndex, Qt


class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self, data=[[]], parent=None):
        super().__init__(parent)
        self.data = data

    def headerData(self, section: int, orientation: Qt.Orientation, role: int):
        if role == QtCore.Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return "Column " + str(section)
            else:
                return "Row " + str(section)

    def columnCount(self, parent=None):
        return len(self.data[0])

    def rowCount(self, parent=None):
        return len(self.data)

    def data(self, index: QModelIndex, role: int):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            col = index.column()
            return str(self.data[row][col])


class MyTableView(QtWidgets.QTableView):
    def __init__(self, parent=None):
        super().__init__(parent)

    def move_row(self, ix, new_ix):
        full_index = self.model().index(0, self.model().rowCount())
        self.model().beginMoveRows(full_index, ix, ix, full_index, new_ix)

        data = self.model().data
        data.insert(new_ix, data.pop(ix))

        self.model().endMoveRows()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    data = [[11, 12, 13, 14, 15],
            [21, 22, 23, 24, 25],
            [31, 32, 33, 34, 35],
            [41, 42, 43, 44, 45],
            [51, 52, 53, 54, 55],
            [61, 62, 63, 64, 65]]

    model = MyTableModel(data)
    view = MyTableView()
    view.setModel(model)

    container = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout()
    container.setLayout(layout)
    layout.addWidget(view)

    button = QtWidgets.QPushButton("Move 3rd row down by 2")
    button.clicked.connect(lambda: view.move_row(2, 2 + 2))
    layout.addWidget(button)

    button = QtWidgets.QPushButton("Move 3rd row up by 1")
    button.clicked.connect(lambda: view.move_row(2, 2 - 1))
    layout.addWidget(button)

    button = QtWidgets.QPushButton("Move 3rd row down by 1 (This fails)")
    button.clicked.connect(lambda: view.move_row(2, 2 + 1))
    layout.addWidget(button)

    container.show()
    sys.exit(app.exec_())

Solution

  • There's another part of the documentation you should be aware of:

    However, when moving rows down in the same parent (sourceParent and destinationParent are equal), the rows will be placed before the destinationChild index. That is, if you wish to move rows 0 and 1 so they will become rows 1 and 2, destinationChild should be 3. In this case, the new index for the source row i (which is between sourceFirst and sourceLast) is equal to (destinationChild-sourceLast-1+i).

    Your solution doesn't consider that, and you can clearly see that if you select the third row and then try to use the first button: while the data is correctly moved, the selection isn't.

    In your case, since you're moving a single row, the solution is simple: add 1 if the movement is to the bottom. Also note that you should use the parent, which, for a table model is always an invalid QModelIndex.

        def move_row(self, ix, new_ix):
            parent = QtCore.QModelIndex()
            if new_ix > ix:
                target = new_ix + 1
            else:
                target = new_ix
            self.model().beginMoveRows(parent, ix, ix, parent, target)
    
            data = self.model().data
            data.insert(new_ix, data.pop(ix))
    
            self.model().endMoveRows()
    

    Consider that, for better compliance with the modularity of OOP, you should create a proper function in the model class, not in the view.