QTableWidget 在用里面的 QLineEdits 清除后加载非常慢

QTableWidget loads very slow after clearing it with QLineEdits inside

我有一个 QTableWidget,里面有 QLineEdits 作为 CellWidgets。 如果我清除 table 并用相同的函数重新填充它,则需要更长的时间才能完成。 在此示例中,差异介于 0.1 和 3.9 秒之间,但在我的实际代码中,差异介于 0.1 秒和 10 分钟之间。

下面是示例代码:

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)

输出:

0.14005303382873535

在使用 QCombobox 并将其设置回“重置”后:

3.9842889308929443

在有人问之前,我用 QLineEdits 填充了 QTableWidget,因为我想使用占位符。

差异不仅是因为您为每个单元格使用单元格小部件(而且,让我说 3000 个小部件很多),而是因为你每次都打电话给 setRowCount()

您还可以看到问题不是在“清除”之后发生的,而是在创建新单元格时发生的:只需删除 __init__ 中对 fillTable 的第一次调用,就会发生相同的延迟.
每次模型布局更改(adding/removing 行或列)时,不仅模型会发生很多事情,显示其内容的视图也会发生很多事情,并且由于您是单独添加行,这会导致更长的时间视图处理其内容所必需的,即使您无法立即看到它(那是因为重新绘制是 排队 并且仅在事件队列被清除后才会发生)。

为了提高性能,在您的情况下,您应该只调用一次 setRowCount() 并显示最终的行数:

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

        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:
            <b>count = int(currentText)
            self.table.setRowCount(count)</b>
            for row in range(0, int(currentText)):
                self.fill_row(row)

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

最后,如果您真的要显示那么多行,我强烈建议您寻找一个替代方案,正如文档对 setIndexWidget()(由 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.

这是因为大量的小部件会导致严重的性能问题(就像你的一样)。

如果您需要的是占位符,那么为每个单元格使用 QLineEdit 是一个 糟糕的 选择,不仅出于性能原因,而且还因为那样您不可以直接访问模型数据,但在此之前您总是需要找到单元格小部件。 更优雅和更可取的解决方案是使用自定义委托,当单元格没有数据时,它将显示占位符文本,并将 CurrentChanged 添加到 table 编辑触发器:

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

一个简单的委托实现可能是这样的:

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))

注意:您不应该使用宽度和高度为 0 的 setGeometry(),并且始终提供默认位置对于大屏幕或多个屏幕的用户来说可能会非常烦人;此外,widthheight 是所有 QWidget 子类的默认属性,不应被自定义属性覆盖。