Search code examples
pyqt5delegatespaintpyside2qtablewidget

How to display a button in each cell of a QTableWidget's column so that it removes its corresponding row when clicked?


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:

  • Using the QTableWidget::setCellWidget method
  • Overriding the paint method and catching the left click event

But:

  • I believe the first alternative is not very clean as I must create the buttons in a for loop and each time a row is added (but after all, I also call openPersistentEditor the same way here).
  • I am wondering if the second alternative is worth the effort. And if it does, how to do it?

Also:

  • I believe my 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 ?
  • I had to override the 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!


Solution

  • 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.