PySide6调用QTextLayout.setFormats高亮文本时文本块不可见问题
如何正确使用
setFormats? 问题描述
程序在QMainWindow中嵌入了QTextEdit控件,需求如下:
- 查找文本中的单词
import - 通过
block.layout.setFormats方法将匹配到的单词高亮为红色 - 要求外观修改不录入撤销-重做历史,且不使用
QSyntaxHighlighter实现
当前遇到异常:找到import单词并执行setFormats调用后,对应的文本块变为不可见,不清楚问题成因。
复现代码
from PySide6 import QtWidgets, QtCore, QtGui class MainWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() self.central_widget = QtWidgets.QWidget(self) self.central_widget.setLayout(QtWidgets.QVBoxLayout(self.central_widget)) self.text_editor = QtWidgets.QTextEdit(self.central_widget) self.central_widget.layout().addWidget(self.text_editor) self.setCentralWidget(self.central_widget) self.text_editor.setFont(QtGui.QFont('Arial', 14)) self.text_editor.textChanged.connect(self.text_changed) @QtCore.Slot() def text_changed(self): word = 'import' text_cursor_before_find_op = self.text_editor.textCursor() self.text_editor.moveCursor(QtGui.QTextCursor.MoveOperation.Start) found = self.text_editor.find(word) if found: text_cursor = self.text_editor.textCursor() text_cursor.setPosition(text_cursor.position()) block = text_cursor.block() position_in_block = text_cursor.positionInBlock() - len(word) format_range = QtGui.QTextLayout.FormatRange() format_range.start = position_in_block format_range.length = len(word) format_range.format = self.text_editor.currentCharFormat() format_range.format.setForeground(QtGui.QColor('#FF0000')) formats = [format_range] block.layout().setFormats(formats) print(position_in_block, repr(block.text()), (format_range.start, format_range.length, format_range.format), block.isValid(), block.isVisible(), sep='\n', end='\n\n') self.text_editor.setTextCursor(text_cursor_before_find_op) app = QtWidgets.QApplication() window = MainWindow() window.show() app.exec()
问题原因
文本块不可见的核心原因是对setFormats接口逻辑理解错误:
QTextLayout.setFormats()不是追加格式,而是全量替换当前文本块的所有渲染格式。原代码只传入了匹配到的import单词对应的格式范围,块内其余文本没有对应的格式配置,Qt渲染时会将无格式的文本判定为透明,最终整个文本块看起来完全消失。- 额外逻辑缺陷:
textChanged信号触发时,文档的布局排版还未完成,此时直接设置的格式会被后续的默认布局流程覆盖,无法持久生效。
修正方案
- 调用
setFormats前先获取文本块现有的全部格式列表,在原有格式基础上追加自定义高亮格式,禁止直接传单一格式段覆盖全块 - 将格式设置逻辑通过
QTimer.singleShot(0, ...)投递到事件循环队列,等当前文本变更的布局流程完成后再执行格式设置 - 每次设置新格式前,先移除之前添加的自定义高亮格式,避免格式残留
- 设置完格式后主动触发对应文本块和视图的重绘,保证渲染更新
修正后可运行代码
from PySide6 import QtWidgets, QtCore, QtGui class MainWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() self.central_widget = QtWidgets.QWidget(self) self.central_widget.setLayout(QtWidgets.QVBoxLayout(self.central_widget)) self.text_editor = QtWidgets.QTextEdit(self.central_widget) self.central_widget.layout().addWidget(self.text_editor) self.setCentralWidget(self.central_widget) self.text_editor.setFont(QtGui.QFont('Arial', 14)) self.target_word = 'import' self.highlight_color = QtGui.QColor('#FF0000') self.text_editor.textChanged.connect(self.schedule_highlight) def schedule_highlight(self): # 等待当前轮次布局完成后再执行高亮,避免格式被覆盖 QtCore.QTimer.singleShot(0, self.apply_highlight) def apply_highlight(self): original_cursor = self.text_editor.textCursor() doc = self.text_editor.document() block = doc.begin() word_len = len(self.target_word) while block.isValid(): layout = block.layout() # 过滤掉之前添加的旧高亮,保留原生格式 valid_formats = [] for fr in layout.formats(): if fr.format.foreground().color() != self.highlight_color: valid_formats.append(fr) # 遍历当前块匹配所有目标单词 block_text = block.text() search_pos = 0 while True: match_pos = block_text.find(self.target_word, search_pos) if match_pos == -1: break # 构造高亮格式段 hl_range = QtGui.QTextLayout.FormatRange() hl_range.start = match_pos hl_range.length = word_len hl_fmt = QtGui.QTextCharFormat() hl_fmt.setForeground(self.highlight_color) hl_range.format = hl_fmt valid_formats.append(hl_range) search_pos = match_pos + word_len # 全量设置格式 layout.setFormats(valid_formats) block = block.next() # 恢复光标位置,更新视图 self.text_editor.setTextCursor(original_cursor) self.text_editor.viewport().update() app = QtWidgets.QApplication() window = MainWindow() window.show() app.exec()
该实现的格式修改直接作用于渲染层,不会修改文档底层内容,因此不会进入撤销/重做栈,完全符合需求。
内容的提问来源于stack exchange,提问作者user51515151
相关产品推荐
相关产品推荐

