QListView加载超长文本行卡顿问题的解决方案咨询
解决QListView加载超长文本行的卡顿问题
核心问题分析
QListView滚动卡顿的根源在于每一行超长文本的渲染开销:滚动时Qt需要实时计算每一行的布局、绘制完整的超长文本——哪怕用户只能看到其中一小部分,这会占用大量CPU资源。另外,一次性加载大量行到内存会增加内存压力,但你测试少量超长行也卡顿,说明渲染是主要瓶颈。
针对性解决方案
1. 仅渲染可见区域的文本片段
不让Qt绘制完整的超长文本,而是根据当前视图的可见区域,只传递需要显示的文本片段给模型/视图。通过自定义QStyledItemDelegate实现:
- 重写
paint()方法,计算当前项的可见矩形,只截取对应区域的文本绘制。 - 重写
sizeHint()方法,提前计算完整文本的高度(可缓存行高避免重复计算)。
示例自定义委托代码:
from PySide2.QtWidgets import QStyledItemDelegate from PySide2.QtGui import QTextLayout, QPainter, QPalette from PySide2.QtCore import Qt, QStyle class LongTextDelegate(QStyledItemDelegate): def paint(self, painter, option, index): text = index.data(Qt.DisplayRole) if not text: super().paint(painter, option, index) return # 获取当前项的可见绘制区域 clip_rect = option.rect painter.save() painter.setClipRect(clip_rect) # 设置选中/正常状态的文本样式 palette = option.palette if option.state & QStyle.State_Selected: painter.fillRect(option.rect, palette.highlight()) painter.setPen(palette.highlightedText().color()) else: painter.setPen(palette.text().color()) # 构建文本布局,仅绘制可见部分 text_layout = QTextLayout(text) text_layout.setFont(option.font) text_layout.beginLayout() line = text_layout.createLine() while line.isValid(): line.setLineWidth(clip_rect.width()) line_y = option.rect.top() + line.y() # 跳过可见区域外的行 if line_y + line.height() < clip_rect.top(): line = text_layout.createLine() continue if line_y > clip_rect.bottom(): break # 绘制当前可见的文本行 painter.drawText(clip_rect.left(), line_y, line.text()) line = text_layout.createLine() text_layout.endLayout() painter.restore() def sizeHint(self, option, index): text = index.data(Qt.DisplayRole) if not text: return super().sizeHint(option, index) # 计算完整文本的高度(可缓存结果优化性能) text_layout = QTextLayout(text) text_layout.setFont(option.font) text_layout.beginLayout() line = text_layout.createLine() total_height = 0 while line.isValid(): line.setLineWidth(option.rect.width()) total_height += line.height() line = text_layout.createLine() text_layout.endLayout() return QtCore.QSize(option.rect.width(), total_height)
在你的ListDemo中使用该委托:
self.listview.setItemDelegate(LongTextDelegate())
2. 按需加载与虚拟滚动优化
如果后续需要加载大量行,虚拟滚动+按需加载是必要的:
- 自定义
QAbstractListModel,不在初始化时加载所有文本,而是在data()方法中按需读取对应行。 - 实现行缓存,避免重复读取文件。
示例按需加载模型代码:
class LazyLoadModel(QtCore.QAbstractListModel): def __init__(self, file_path, parent=None): super().__init__(parent) self.file_path = file_path self.line_count = 0 self.cache = {} # 缓存最近访问的100行 self._count_total_lines() def _count_total_lines(self): # 预计算总行数(可放到后台线程执行避免阻塞UI) with open(self.file_path, 'r') as f: self.line_count = sum(1 for _ in f) def rowCount(self, parent=QtCore.QModelIndex()): return self.line_count def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid() or role != QtCore.Qt.DisplayRole: return None row = index.row() # 优先从缓存读取 if row in self.cache: return self.cache[row] # 按需读取对应行并缓存 with open(self.file_path, 'r') as f: for i, line in enumerate(f): if i == row: text = line.rstrip() # 缓存满100行时删除最早的条目 if len(self.cache) >= 100: self.cache.pop(next(iter(self.cache))) self.cache[row] = text return text return None
替换原模型:
self.model = LazyLoadModel('new 6.txt') self.listview.setModel(self.model)
3. 关闭不必要的视图特性
- 禁用
setUniformItemSizes(True):该设置会丢失水平滚动条,且行高不一致时会增加布局计算开销。 - 启用像素级滚动:
self.listview.setVerticalScrollMode(QListView.ScrollPerPixel),滚动更流畅。 - 禁用文本换行:
self.listview.setWordWrap(False),减少布局计算复杂度。
4. 参考klogg的实现思路
klogg的流畅核心在于:
- 内存映射文件:直接映射文件到内存,避免一次性加载所有内容,读取速度快且内存占用低。
- 按需渲染:只绘制当前可见区域的文本,预渲染附近行做缓存。
- 异步加载:后台线程读取文件,不阻塞UI线程。
优化后的完整测试代码
import sys from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication, QListView, QStyledItemDelegate from PySide2 import QtCore from PySide2.QtGui import QTextLayout, QPainter, QPalette, QStyle class LongTextDelegate(QStyledItemDelegate): def paint(self, painter, option, index): text = index.data(QtCore.Qt.DisplayRole) if not text: super().paint(painter, option, index) return clip_rect = option.rect painter.save() painter.setClipRect(clip_rect) palette = option.palette if option.state & QStyle.State_Selected: painter.fillRect(option.rect, palette.highlight()) painter.setPen(palette.highlightedText().color()) else: painter.setPen(palette.text().color()) text_layout = QTextLayout(text) text_layout.setFont(option.font) text_layout.beginLayout() line = text_layout.createLine() while line.isValid(): line.setLineWidth(clip_rect.width()) line_y = option.rect.top() + line.y() if line_y + line.height() < clip_rect.top(): line = text_layout.createLine() continue if line_y > clip_rect.bottom(): break painter.drawText(clip_rect.left(), line_y, line.text()) line = text_layout.createLine() text_layout.endLayout() painter.restore() def sizeHint(self, option, index): text = index.data(QtCore.Qt.DisplayRole) if not text: return super().sizeHint(option, index) text_layout = QTextLayout(text) text_layout.setFont(option.font) text_layout.beginLayout() line = text_layout.createLine() total_height = 0 while line.isValid(): line.setLineWidth(option.rect.width()) total_height += line.height() line = text_layout.createLine() text_layout.endLayout() return QtCore.QSize(option.rect.width(), total_height) class LazyLoadModel(QtCore.QAbstractListModel): def __init__(self, file_path, parent=None): super().__init__(parent) self.file_path = file_path self.line_count = 0 self.cache = {} self._count_total_lines() def _count_total_lines(self): with open(self.file_path, 'r') as f: self.line_count = sum(1 for _ in f) def rowCount(self, parent=QtCore.QModelIndex()): return self.line_count def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid() or role != QtCore.Qt.DisplayRole: return None row = index.row() if row in self.cache: return self.cache[row] with open(self.file_path, 'r') as f: for i, line in enumerate(f): if i == row: text = line.rstrip() if len(self.cache) >= 100: self.cache.pop(next(iter(self.cache))) self.cache[row] = text return text return None class ListDemo(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Test") self.resize(1000,940) self.listview = QListView() self.model = LazyLoadModel('new 6.txt') self.listview.setModel(self.model) self.listview.setItemDelegate(LongTextDelegate()) self.listview.setWordWrap(False) self.listview.setVerticalScrollMode(QListView.ScrollPerPixel) self.load_lv = QPushButton("Refresh File") layout = QVBoxLayout() layout.addWidget(self.listview) layout.addWidget(self.load_lv) self.setLayout(layout) self.load_lv.clicked.connect(self.refresh_model) def refresh_model(self): self.model.beginResetModel() self.model._count_total_lines() self.model.cache.clear() self.model.endResetModel() def closeEvent(self, event): print("CLOSE EVENT") def main(): app = QApplication(sys.argv) win = ListDemo() win.show() sys.exit(app.exec_()) if __name__ == "__main__": main()
总结
- 优先通过自定义委托减少不必要的文本渲染,这是解决卡顿的核心。
- 结合按需加载模型,避免一次性加载所有数据到内存。
- 关闭不必要的视图特性,优化滚动模式。
内容的提问来源于stack exchange,提问作者SimpleOne
相关产品推荐
相关产品推荐

