QTableWidget loads very slow after clearing it with QLineEdits inside

381 Views Asked by At

I have a QTableWidget with QLineEdits as CellWidgets inside of it. If I clear the table and refill it with the same function it needs much longer to finish. In this example the difference is between 0.1 and 3.9 seconds, but in my real code the difference is between 0.1 seconds and 10 minutes.

So here is the example code:

class Test_Layout(QWidget):
    def __init__(self, parent=None):
        super(Test_Layout, self).__init__(parent=None)

        self.left = 0
        self.top = 0
        self.width = 0
        self.height = 0

        self.initUI()

        self.isMaximized()

    def initUI(self):
        self.setGeometry(self.left, self.top, self.width, self.height)

        self.createTable()

        start_time = time.time()
        self.fillTable()
        print(time.time() - start_time)

        self.combo = QComboBox(self)
        self.combo.addItem("Reset")
        for i in range(0, 5):
            self.combo.addItem(str(i))
        self.combo.currentTextChanged.connect(self.on_combo_changed)

        self.vbox = QVBoxLayout()
        self.vbox.addWidget(self.combo)
        self.vbox.addWidget(self.table)

        self.setLayout(self.vbox)

    def fill_row(self, row):
        self.table.insertRow(row)

        placeholder = QLineEdit()
        self.table.setCellWidget(row, 0, placeholder)
        placeholder = QLineEdit()
        self.table.setCellWidget(row, 1, placeholder)
        placeholder = QLineEdit()
        self.table.setCellWidget(row, 2, placeholder)
        
    def on_combo_changed(self, currentText):
        self.table.setRowCount(0)

        if currentText == "Reset":
            start_time = time.time()
            self.fillTable()
            print(time.time() - start_time)
        else:
            for row in range(0, int(currentText)):
                self.fill_row(row)

    def createTable(self):
        self.table = QTableWidget()
        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels([
            "LineEdit0",
            "LineEdit1",
            "LineEdit2",
        ])

        header = self.table.horizontalHeader()
        for i in range(0, 3):
            header.setSectionResizeMode(i, QHeaderView.ResizeToContents)

    def fillTable(self):
        for row in range(0, 1000):
            self.fill_row(row)

Output:

0.14005303382873535

And after using the QCombobox and setting it back to "Reset":

3.9842889308929443

And before somebody asks, I fill the QTableWidget with QLineEdits, because I want to use placeholders.

1

There are 1 best solutions below

0
On BEST ANSWER

The difference is not only due to the fact that you're using cell widgets for each cell (and, let me say that 3000 widgets are a lot), but because you're calling setRowCount() each time.

You can also see that the problem happens not after "clearing", but just when creating the new cells: just remove the first call to fillTable in the __init__ and the same delay occurs.
Each time the model layout changes (by adding/removing rows or columns), lots of things happen not only for the model, but for the view that shows its contents, and since you are adding rows individually, this results in longer time required for the view to process its contents, even if you cannot see it instantly (and that's because the repainting is queued and only happens as soon as the event queue is cleared).

To improve performance, in your case, you should call setRowCount() only once with the final number of rows that are going to be shown:

    def fill_row(self, row):
        # comment or remove the following line
        # self.table.insertRow(row)

        placeholder = QLineEdit()
        self.table.setCellWidget(row, 0, placeholder)
        placeholder = QLineEdit()
        self.table.setCellWidget(row, 1, placeholder)
        placeholder = QLineEdit()
        self.table.setCellWidget(row, 2, placeholder)

    def on_combo_changed(self, currentText):
        self.table.setRowCount(0)

        if currentText == "Reset":
            start_time = time.time()
            self.fillTable()
            print(time.time() - start_time)
        else:
            count = int(currentText)
            self.table.setRowCount(count)
            for row in range(0, int(currentText)):
                self.fill_row(row)

    def fillTable(self):
        self.table.setRowCount(1000)
        for row in range(0, 1000):
            self.fill_row(row)

Finally, if you're really going to show that many rows, I strongly suggest to find an alternative, as the documentation explains for setIndexWidget() (which is internally called by setCellWidget()):

This function should only be used to display static content within the visible area corresponding to an item of data. If you want to display custom dynamic content or implement a custom editor widget, subclass QStyledItemDelegate instead.

This is because large amounts of widgets will cause drastic performances issues (exactly like yours).

If what you need is a placeholder, using a QLineEdit for each cell is a bad choice, not only for performance reasons, but also because in that way you don't have direct access to the model data, but you would always need to find the cell widget before that. A more elegant and preferable solution is to use a custom delegate, which will show the placeholder text when there is no data for the cell, and add CurrentChanged to the table edit triggers:

self.table.setEditTriggers(self.table.editTriggers() | self.table.CurrentChanged)

A simple delegate implementation could be like this:

class PlaceholderDelegate(QStyledItemDelegate):
    def __init__(self, placeholder='', parent=None):
        super().__init__(parent)
        self.placeholder = placeholder

    def createEditor(self, parent, option, index):
        editor = super().createEditor(parent, option, index)
        if isinstance(editor, QLineEdit):
            editor.setPlaceholderText(self.placeholder)
        return editor

    def paint(self, painter, option, index):
        super().paint(painter, option, index)
        if not index.data():
            try:
                # placeholderText palette role was introduced on Qt 5.12
                color = option.palette.placeholderText().color()
            except:
                color = option.palette.text().color()
                color.setAlpha(128)
            painter.setPen(color)
            style = option.widget.style()
            margin = style.pixelMetric(style.PM_FocusFrameHMargin, option, option.widget)
            rect = option.rect.adjusted(margin, 0, -margin, 0)
            text = option.fontMetrics.elidedText(self.placeholder, option.textElideMode, rect.width())
            painter.drawText(rect, option.displayAlignment, text)


class Test_Layout(QWidget):
    # ...
    def createTable(self):
        # ...
        for i in range(0, 3):
            header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
            self.table.setItemDelegateForColumn(
                i, PlaceholderDelegate('empty {}'.format(i + 1), self.table))

Note: you should not use setGeometry() with 0 width and height, and always provide a default position can be very annoying for users with large screens or more than one screen; also, width and height are default properties for all QWidget subclasses, and should never be overwritten with custom attributes.