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:
CodePudding user response:
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 alayoutAboutToBeChanged
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 theviewport()
); 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 (seebreak
andcontinue
Statements, andelse
Clauses on Loops);