0

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!

Skryge
  • 81
  • 1
  • 9
  • What's so special about deleting rows that it needs a dedicated button in *every* row? Why not just use a context menu? – ekhumoro Mar 21 '22 at 00:26
  • @Skryge The first method doesn't change that much from opening the persistent editor; in both cases you should explicitly set the widget or open the editor. Consider that you could just create a function that opens the editor and connect it to the [`rowsInserted`](https://doc.qt.io/qt-5/qabstractitemmodel.html#rowsInserted) signal, so that they are automatically added any time a new row is added. The second approach is absolutely *wrong*: not only you cannot use the paint event to get a mouse event, but paint functions should *always* **only** paint and do absolutely *nothing* else. – musicamante Mar 21 '22 at 00:53
  • @ekhumoro I did not think about it. It would be interesting as I would not need to handle the extra column anymore when filling my table. Could you help me to achieve this please? – Skryge Mar 21 '22 at 15:03
  • @musicamante 1. Ok, I will try this solution. 2. Sorry if I was not clear. I meant overriding paint method to paint the button and overriding the table event filter or the good event handler to react accordingly. – Skryge Mar 21 '22 at 15:43
  • Anything about optimizing my `remove_row` method ? – Skryge Mar 21 '22 at 16:01
  • @musicamante Finally, I don't see the benefit of connecting `openPersistentEditor` to `rowsInserted` because it does not work when the table is filled but only when a row is added. For this purpose, my `add_row` method already does the job. – Skryge Mar 21 '22 at 17:03
  • You can add one button that sits on the right edge of table and follows vertical mouse position (jumps between rows) to `viewport()` of QTableWidget as a child widget and make it "remember" current row. Thats how I did. Unfortunately I used c++, so I cant give you the code. – mugiseyebrows Mar 21 '22 at 18:08
  • @mugiseyebrows Interesting! Could you send me the code anyway so that I try to convert it ? – Skryge Mar 21 '22 at 19:58
  • @Skryge https://github.com/mugiseyebrows/tablebuttons-demo.git https://github.com/mugiseyebrows/tablebuttons.git – mugiseyebrows Mar 22 '22 at 12:02

1 Answers1

0

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.

Skryge
  • 81
  • 1
  • 9