I want to display a button in each cell of a QTableWidget
's column. Each button, when clicked, must remove its corresponding row in the table.
To do so, I created a RemoveRowDelegate
class with the button as editor and used the QAbstractItemView::openPersistentEditor
method in a CustomTable
class to display the button permanently.
class RemoveRowDelegate(QStyledItemDelegate):
def __init__(self, parent, cross_icon_path):
super().__init__(parent)
self.cross_icon_path = cross_icon_path
self.table = None
def createEditor(self, parent, option, index):
editor = QToolButton(parent)
editor.setStyleSheet("background-color: rgba(255, 255, 255, 0);") # Delete borders but maintain the click animation (as opposed to "border: none;")
pixmap = QPixmap(self.cross_icon_path)
button_icon = QIcon(pixmap)
editor.setIcon(button_icon)
editor.clicked.connect(self.remove_row)
return editor
# Delete the corresponding row
def remove_row(self):
sending_button = self.sender()
for i in range(self.table.rowCount()):
if self.table.cellWidget(i, 0) == sending_button:
self.table.removeRow(i)
break
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None):
super().__init__(parent)
self.columns = []
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
if df is not None:
self.fill(df)
# Build the table from a pandas df
def fill(self, df):
self.columns = [''] + list(df.columns)
nb_rows, _ = df.shape
nb_columns = len(self.columns)
self.setRowCount(nb_rows)
self.setColumnCount(nb_columns)
self.setHorizontalHeaderLabels(self.columns)
for i in range(nb_rows):
self.openPersistentEditor(self.model().index(i, 0))
for j in range(1, nb_columns):
item = df.iloc[i, j-1]
table_item = QTableWidgetItem(item)
self.setItem(i, j, table_item)
def add_row(self):
nb_rows = self.rowCount()
self.insertRow(nb_rows)
self.openPersistentEditor(self.model().index(nb_rows, 0))
def setItemDelegateForColumn(self, column_index, delegate):
super().setItemDelegateForColumn(column_index, delegate)
delegate.table = self
I set the delegate for the first column of the table and build the latter from a pandas
dataframe:
self.table = CustomTable() # Here, self is my user interface
remove_row_delegate = RemoveRowDelegate(self, self.cross_icon_path)
self.table.setItemDelegateForColumn(0, remove_row_delegate)
self.table.fill(df)
For now, this solution does the job but I think of several other possibilities:
QTableWidget::setCellWidget
methodpaint
method and catching the left click eventBut:
openPersistentEditor
the same way here).Also:
remove_row
method can be optimized as I iterate over all rows (that is one of the reasons why I thought about the second alternative). Would you have a better suggestion ?setItemDelegateForColumn
method so that I can access the table from the RemoveRowDelegate
class. Can it be avoided ?Any other remark that you think might be of interest would be greatly appreciated!
As suggested by @ekhumoro, I finally used a context menu:
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None, add_icon_path=None, remove_icon_path=None):
super().__init__(parent)
self.add_icon_path = add_icon_path
self.remove_icon_path = remove_icon_path
# Activation of customContextMenuRequested signal and connecting it to a method that displays a context menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos))
def show_context_menu(self, pos):
idx = self.indexAt(pos)
if idx.isValid():
row_idx = idx.row()
# Creating context menu and personalized actions
context_menu = QMenu(parent=self)
if self.add_icon_path:
pixmap = QPixmap(self.add_icon_path)
add_icon = QIcon(pixmap)
add_row_action = QAction('Insert a line', icon=add_icon)
else:
add_row_action = QAction('Insert a line')
add_row_action.triggered.connect(lambda: self.insertRow(row_idx))
if self.remove_icon_path:
pixmap = QPixmap(self.remove_icon_path)
remove_icon = QIcon(pixmap)
remove_row_action = QAction('Delete the line', icon=remove_icon)
else:
remove_row_action = QAction('Delete the line')
remove_row_action.triggered.connect(lambda: self.removeRow(row_idx))
context_menu.addAction(add_row_action)
context_menu.addAction(remove_row_action)
# Displaying context menu
context_menu.exec_(self.mapToGlobal(pos))
Moreover, note that using QTableWidget::removeRow
method is more optimized than my previous method. One just need to get the row index properly from the click position thanks to QTableWidget::indexAt
method.