You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5批量填充QTableView时减少过量paint()绘制调用的问题咨询

核心问题原因

你遇到的性能问题核心来自三个高频开销点:

  1. 逐行插入数据,每次插入行、调整行高都会触发视图重绘,单次批次触发数百次重绘请求
  2. Delegate的paint和sizeHint方法每次都新建QTextDocument,对象创建、HTML解析开销极高
  3. 列宽变化时的行高调整逻辑遍历全表所有行,行数增长后开销线性上涨

具体优化方案

1. 批量插入数据,减少重绘触发次数

不要逐行调用insertRow,改用insertRows一次性插入整批次的所有行,插入期间关闭视图更新,完成所有数据写入和行高调整后再开启更新,单次批次仅触发1次重绘。
修改populate_table方法如下:

def populate_table(self, batch):
    row_count = self.table_view.model().rowCount()
    batch_len = len(batch)
    # 插入前关闭视图更新
    self.table_view.setUpdatesEnabled(False)
    # 一次性插入整批次行
    self.table_view.model().insertRows(row_count, batch_len)
    # 批量设置单元格数据
    for n_row, row in enumerate(batch):
        current_row = row_count + n_row
        self.table_view.model().setItem(current_row, 0, QtGui.QStandardItem(row[0]))
        self.table_view.model().setItem(current_row, 1, QtGui.QStandardItem(row[1]))
    # 仅给新插入的行计算行高,不要遍历全表
    for n_row in range(row_count, row_count + batch_len):
        self.table_view.resizeRowToContents(n_row)
    # 恢复视图更新,触发一次重绘即可
    self.table_view.setUpdatesEnabled(True)
    
    self.n_batch += 1
    print(f'self.n_batch {self.n_batch}')    

    if row_count == 0:
        self.table_view.setColumnWidth(0, 400)
        self.table_view.setColumnWidth(1, 400)

2. 缓存QTextDocument,避免重复解析HTML

Delegate中每次调用paint和sizeHint都新建QTextDocument是最大的性能浪费,增加缓存逻辑复用已创建的文档对象,仅当单元格内容变化时才更新缓存:
修改SegmentsTableViewDelegate如下:

class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
    class EditorDocument(QtGui.QTextDocument):
        def __init__(self, parent):
            super().__init__(parent)
            self.setDocumentMargin(0)
            self.contentsChange.connect(self.contents_change)
            self.height = None
            parent.setDocument(self)
        
        def contents_change(self, position, chars_removed, chars_added):
            def resize_func():
                if self.size().height() != self.height:
                    doc_size = self.size()
                    self.parent().resize(int(doc_size.width()), int(doc_size.height()))
            QtCore.QTimer.singleShot(0, resize_func)
    
    def __init__(self, *args):
        super().__init__(*args)
        self.pm_index_to_editor_dict = {}
        self.paint_count = 0
        # 新增QTextDocument缓存,key为(行号,列号)
        self.doc_cache = {}
    
    def createEditor(self, parent, option, index):
        class Editor(QtWidgets.QTextEdit):
            def resizeEvent(self, event):
                super().resizeEvent(event)
                parent.parent().resizeRowToContents(index.row())
                
        pm_index = QtCore.QPersistentModelIndex(index)
        if pm_index in self.pm_index_to_editor_dict:
            editor = self.pm_index_to_editor_dict[pm_index]
        else:
            editor = Editor(parent)
            editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
            editor.setFrameShape(0)
            self.pm_index_to_editor_dict[pm_index] = editor                    
        SegmentsTableViewDelegate.EditorDocument(editor)
        return editor
    
    def destroyEditor(self, editor, index):
        super().destroyEditor(editor, index)
        pm_index = QtCore.QPersistentModelIndex(index)
        del self.pm_index_to_editor_dict[pm_index]
        self.parent().resizeRowToContents(index.row())
        # 编辑完成后清除对应缓存
        cache_key = (index.row(), index.column())
        if cache_key in self.doc_cache:
            del self.doc_cache[cache_key]
        
    def paint(self, painter, option, index):
        self.paint_count += 1
        if self.paint_count % 100 == 0:
            print(f'self.paint_count {self.paint_count} index.row() {index.row()}')
        
        self.initStyleOption(option, index)
        painter.save()
        cache_key = (index.row(), index.column())
        # 优先用缓存的文档,内容变化才重新生成
        if cache_key not in self.doc_cache or self.doc_cache[cache_key]['text'] != option.text:
            doc = QtGui.QTextDocument()
            doc.setDocumentMargin(0)
            doc.setDefaultFont(option.font)
            doc.setTextWidth(option.rect.width())
            doc.setHtml(option.text)
            self.doc_cache[cache_key] = {
                'doc': doc,
                'text': option.text
            }
        else:
            doc = self.doc_cache[cache_key]['doc']
            
        option.text = ""
        option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
        painter.translate(option.rect.left(), option.rect.top())
        doc.drawContents(painter)
        painter.restore()
            
    def sizeHint(self, option, index):
        self.initStyleOption(option, index)
        pm_index = QtCore.QPersistentModelIndex(index)
        if pm_index in self.pm_index_to_editor_dict:
            doc = self.pm_index_to_editor_dict[pm_index].document()
        else:
            cache_key = (index.row(), index.column())
            if cache_key not in self.doc_cache or self.doc_cache[cache_key]['text'] != option.text:
                doc = QtGui.QTextDocument()
                doc.setTextWidth(option.rect.width())
                doc.setDefaultFont(option.font)
                doc.setDocumentMargin(0)
                doc.setHtml(option.text)
                self.doc_cache[cache_key] = {
                    'doc': doc,
                    'text': option.text
                }
            else:
                doc = self.doc_cache[cache_key]['doc']
        return QtCore.QSize(int(doc.idealWidth()), int(doc.size().height()))

3. 优化列宽变化时的行高调整逻辑

你当前resizeRowsToContents方法会遍历全表所有行,列宽变化时开销极高,修改为仅调整视口内可见行的高度即可:

class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        super().__init__(parent)
        self.setItemDelegate(SegmentsTableViewDelegate(self))
        self.setModel(QtGui.QStandardItemModel())
        v_header =  self.verticalHeader()
        v_header.setMinimumSectionSize(5)
        v_header.sectionHandleDoubleClicked.disconnect()
        v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
        self.horizontalHeader().sectionResized.connect(self.resizeVisibleRowsToContents)        
    
    def resizeRowToContents(self, row):
        super().resizeRowToContents(row)
        self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))

    # 替换原resizeRowsToContents,仅调整可见行
    def resizeVisibleRowsToContents(self):
        header = self.verticalHeader()
        # 计算视口内可见的起止行号
        start_row = self.rowAt(0)
        end_row = self.rowAt(self.viewport().height())
        # 处理边界情况
        if start_row < 0:
            start_row = 0
        if end_row < 0 or end_row >= self.model().rowCount():
            end_row = self.model().rowCount() - 1
        for row in range(start_row, end_row + 1):
            hint = self.sizeHintForRow(row)
            header.resizeSection(row, hint)

4. 高阶优化(数据量超过1万行时建议采用)

QStandardItemModel的内存和性能开销较高,替换为自定义QAbstractTableModel,直接存储原始字符串数据,不需要封装为QStandardItem,性能可以提升3~5倍。

内容的提问来源于stack exchange,提问作者mike rodent

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 16:45:03