PyQt5批量填充QTableView时减少过量paint()绘制调用的问题咨询
核心问题原因
你遇到的性能问题核心来自三个高频开销点:
- 逐行插入数据,每次插入行、调整行高都会触发视图重绘,单次批次触发数百次重绘请求
- Delegate的
paint和sizeHint方法每次都新建QTextDocument,对象创建、HTML解析开销极高 - 列宽变化时的行高调整逻辑遍历全表所有行,行数增长后开销线性上涨
具体优化方案
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
相关产品推荐
相关产品推荐

