Unable remove row in proper order from qtableview

Question:

Press create button(Alt + C) and it will create a row(Port) in the table and delete button is to remove the corresponding row(Port) from table.

When delete button is pressed deleteButtonPressed method is called. This method is suppose to remove the corresponding row from tableview but it removes the last row from tableview. Doesn’t matter which button is pressed the deleteButtonPressed always removes the last row for tableview(which is wrong) and removes the respective row from model.rows(which is correct).

def deleteButtonPressed(self):
    # print('delete button  pressed', source)
    index = None
    port: Port
    for ind, port in enumerate(self.model.rows):
        if port.deleteButton is self.sender():
            index = ind
            break
    if index is not None:
        self.table.model().removeRow(index, QModelIndex())
        optionRow = self.model.rows.pop(index)
        self.table.model().layoutChanged.emit()
    else:
        print('Error index is None')

How to fix removing of wrong row when delete button is pressed from tableview?

Source File

import sys
import uuid
from typing import Any, Union
import random

from PySide6.QtWidgets import (QTableView, QWidget, QMainWindow, QApplication,
                               QPushButton, QToolBar, QVBoxLayout, QDoubleSpinBox)
from PySide6.QtCore import (Qt, QAbstractTableModel, QModelIndex, QPersistentModelIndex)
from PySide6.QtGui import (QAction)


##### Port ######

class Port:
    def __init__(self):
        self.id = str(uuid.uuid4())
        self.age = random.randint(22, 80)

        self.amt = QDoubleSpinBox()
        self.amt.setRange(0.0, float('inf'))
        self.amt.setValue(random.randint(0, 2e+3))
        self.amt.setSingleStep(1.0)
        self.amt.setPrefix('$')

        self.deleteButton = QPushButton('Del')

    def getId(self):
        return self.id

    def getAge(self):
        return self.age

    def setAge(self, age: int):
        self.age = age

    def getAmount(self):
        return self.amt.value()

    def setAmount(self, amt: float):
        self.amt.setValue(amt)


class TableModel(QAbstractTableModel):
    def __init__(self):
        super().__init__()
        self.columnsName = ['id', 'Age', 'Amt', 'Delete']
        self.rows: list[Port] = list()

    def data(self, index: Union[QModelIndex,
                                QPersistentModelIndex], role: int = ...) -> Any:
        row = index.row()
        col = index.column()
        port = self.rows[row]

        if role == Qt.DisplayRole:
            if col == 0:
                return port.getId()
            if col == 1:
                return port.getAge()

    def rowCount(self, parent: Union[QModelIndex, QPersistentModelIndex] = ...) -> int:
        return len(self.rows)

    def columnCount(self, parent: Union[QModelIndex, QPersistentModelIndex] = ...) -> int:
        return len(self.columnsName)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any:
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return self.columnsName[section]
            if orientation == Qt.Vertical:
                return section + 1


##### MainWindow #####
class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("TableWindow")
        self.widget = QWidget()
        self.widget.layout = QVBoxLayout()
        self.widget.setLayout(self.widget.layout)

        ######### Initializing toolbar ########
        self.toolbar = QToolBar()
        self.initToolBar()
        self.addToolBar(self.toolbar)
        ########################################

        ######## Initializing table ###########
        self.table = QTableView()
        self.model = TableModel()
        self.table.setModel(self.model)
        self.widget.layout.addWidget(self.table)
        #######################################

        self.setCentralWidget(self.widget)

    def initToolBar(self):
        self.createAction = QAction("Create")
        self.createAction.setShortcut("Alt+C")
        self.toolbar.addAction(self.createAction)
        self.createAction.triggered.connect(self.createActionPressed)

    def createActionPressed(self):
        print('Create')
        self.port = Port()
        self.port.deleteButton.pressed.connect(self.deleteButtonPressed)
        self.appendRow(self.port)

    def appendRow(self, port: Port):
        index = len(self.table.model().rows)
        self.table.model().rows.append(port)
        self.table.setIndexWidget(self.table.model().index(index, 2), port.amt)
        self.table.setIndexWidget(self.table.model().index(index, 3), port.deleteButton)
        self.table.model().layoutChanged.emit()
        self.table.repaint()

    def deleteButtonPressed(self):
        # print('delete button  pressed', source)
        index = None
        port: Port
        for ind, port in enumerate(self.model.rows):
            if port.deleteButton is self.sender():
                index = ind
                break
        if index is not None:
            self.table.model().removeRow(index, QModelIndex())
            optionRow = self.model.rows.pop(index)
            self.table.model().layoutChanged.emit()
        else:
            print('Error index is None')


if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    app.exec()

Output:

Table window

Asked By: devp

||

Answers:

Changing the size and layout of the model should always use the provided API.

As explained in the "subclassing" section of the QAbstractTableModel documentation:

Models that provide interfaces to resizable data structures can provide implementations of insertRows(), removeRows(), insertColumns(), and removeColumns().

While providing those implementation is not a requirement, it is required that you still use the insertion/removal functions of the API:

Simply removing items from the internal list is not sufficient, as removeRows() (which is always called from the convenience function removeRow()) must be implemented if you intend to use those functions to change the model size.

In your case, what you’re experiencing is caused by the fact that you’re externally emitting layoutChanged which internally causes a "relayout" of the indexes, but that is a wrong approach, because:

  • layoutChanged is mostly about the layout (i.e., sorting), not the model size; also, that signal is normally expected after emitting a layoutAboutToBeChanged signal; coincidentally, this causes the view to compute again its layout and size, but this is not enough: its effect may seem to achieve your purpose, but it has the wrong result;
  • you’re just removing the items from the list, not the model indexes, nor their widgets (they are children of a Qt object, so they are not destroyed); the result is that when you remove an item, the model index still exists, and its related widget as well: you’re not actually removing the chosen model index you’re removing the last (because the layout change causes a recall on rowCount());

For instance, if you remove the first row, the result is that the corresponding data fields are removed, but their row indexes remain. The layoutChanged signal causes the view to recompute the layout and ask the model about its size, but now the button that used to be in for the first row of the model is now used for the previous second row, which is now the first. This means that your for loop will never get the sender of the signal, because that reference doesn’t exist anymore in the list.

You can clearly see the result if you add a "row argument" to the Port() and use that argument for the button text:

class Port:
    def __init__(self, index):
        # ...
        self.deleteButton = QPushButton('Del {}'.format(index + 1))


class MainWindow(QMainWindow):
    # ...
    def createActionPressed(self):
        print('Create')
        self.port = Port(len(self.table.model().rows))
        self.port.deleteButton.pressed.connect(self.deleteButtonPressed)
        self.appendRow(self.port)

With the above code, you’ll clearly see that when you remove a row, the buttons remain (up to the rows length). The only "change" is that, since the rowCount has changed (as consequence of the query after layoutChanged), the "last" row has been destroyed, and its widgets along with it.

So, the solution is quite simple: use the existing API.

class TableModel(QAbstractTableModel):
    # ...
    def removeRows(self, row, count, parent=QModelIndex()):
        self.beginRemoveRows(parent, row, row + count - 1)
        for r in range(count):
            del self.rows[row]
        self.endRemoveRows()
        return True

Be aware that this also means that you should properly implement insertRows() or, at least, a function that uses beginInsertRows() and endInsertRows(). Since you are using custom objects, you can use an explicit function:

class TableModel(QAbstractTableModel):
    # ...
    def insertPort(self, row, port):
        self.beginInsertRows(QModelIndex(), row, row)
        self.rows.append(port)
        self.endInsertRows()


class MainWindow(QMainWindow):
    # ...
    def createActionPressed(self):
        print('Create')
        model = self.table.model()
        index = len(model.rows)
        port = Port()
        port.deleteButton.pressed.connect(self.deleteButtonPressed)
        model.insertPort(index, port)
        self.table.setIndexWidget(model.index(index, 2), port.amt)
        self.table.setIndexWidget(model.index(index, 3), port.deleteButton)

Notes:

  • most of the times, setting an instance attribute for a dynamic object is pointless, since it will always be overwritten; just use a local variable as above, unless you really need an easy reference to the last instance (but be aware that you shall also create a default None value for that attribute);
  • calling repaint on a view is useless, mostly because that would just cause a repaint on the "container" of the scroll area, not its contents (which is the viewport()); doing that for model changes is also pointless, as those changes always cause an update on the viewport, as long as you’re using the proper API calls;
  • you don’t need a default variable for a for/while loop that breaks when the variable is set: just use the else condition at the end of the loop (see break and continue Statements, and else Clauses on Loops);
Answered By: musicamante
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.