我构建了一个自定义的 PlainTextEdit 作为日志查看器小部件。我有一个外部 lineEdit 小部件来指定搜索模式(类似于您使用 ctrl+f 在网站上查找文本的方式)。lineEdits textChanged 信号连接到该_find_text()
方法。textEdit 应该突出显示相应的匹配项。但是,该方法似乎存在一些问题_clear_search_data()
,因为在先前匹配期间使用 self._highlight_cursor 进行的选择继续保持突出显示。
class LogTextEdit(QtWidgets.QPlainTextEdit):
"""
"""
mousePressedSignal = QtCore.Signal(object)
matchCounterChanged = QtCore.Signal(tuple)
def __init__(self, parent):
"""
"""
super(LogTextEdit, self).__init__(parent)
# some settings
self.setReadOnly(True)
self.setMaximumBlockCount(20000)
self.master_document = QtGui.QTextDocument() # always log against master
self.master_doclay = QtWidgets.QPlainTextDocumentLayout(self.master_document)
self.master_document.setDocumentLayout(self.master_doclay)
self.proxy_document = QtGui.QTextDocument() # display the filtered document
self.proxy_doclay = QtWidgets.QPlainTextDocumentLayout(self.proxy_document)
self.proxy_document.setDocumentLayout(self.proxy_doclay)
self.setDocument(self.proxy_document)
# members
self._matches = []
self._current_match = 0
self._current_search = ("", 0, False)
self._content_timestamp = 0
self._search_timestamp = 0
self._first_visible_index = 0
self._last_visible_index = 0
self._matches_to_highlight = set()
self._matches_label = QtWidgets.QLabel()
self._cursor = self.textCursor()
pos = QtCore.QPoint(0, 0)
self._highlight_cursor = self.cursorForPosition(pos)
# text formatting related
self._format = QtGui.QTextCharFormat()
self._format.setBackground(QtCore.Qt.red)
self.font = self.document().defaultFont()
self.font.setFamily('Courier New') # fixed width font
self.document().setDefaultFont(self.font)
self.reset_text_format = QtGui.QTextCharFormat()
self.reset_text_format.setFont(self.document().defaultFont())
# right click context menu
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.copy_action = QtWidgets.QAction('Copy', self)
self.copy_action.setStatusTip('Copy Selection')
self.copy_action.setShortcut('Ctrl+C')
self.addAction(self.copy_action)
# Initialize state
self.updateLineNumberAreaWidth()
self.highlightCurrentLine()
# Signals
# linearea
self.blockCountChanged.connect(self.updateLineNumberAreaWidth)
self.updateRequest.connect(self.updateLineNumberArea)
self.cursorPositionChanged.connect(self.highlightCurrentLine)
# copy
self.customContextMenuRequested.connect(self.context_menu)
self.copy_action.triggered.connect(lambda: self.copy_selection(QtGui.QClipboard.Mode.Clipboard))
def appendMessage(self, msg:str, document: QtGui.QTextDocument):
cursor = QtGui.QTextCursor(document)
cursor.movePosition(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.MoveAnchor)
cursor.beginEditBlock()
cursor.insertBlock()
cursor.insertText(msg)
cursor.endEditBlock()
self._content_timestamp = time.time()
def _move_to_next_match(self):
"""
Moves the cursor to the next occurrence of search pattern match,
scrolling up/down through the content to display the cursor position.
When the cursor is not set and this method is called, the cursor will
be moved to the first match. Subsequent calls move the cursor through
the next matches (moving forward). When the cursor is at the last match
and this method is called, the cursor will be moved back to the first
match. If there are no matches, this method does nothing.
"""
if not self._matches:
return
if self._current_match >= len(self._matches):
self._current_match = 0
self._move_to_match(self._matches[self._current_match][0],
self._matches[self._current_match][1])
self._current_match += 1
def _move_to_prev_match(self):
"""
Moves the cursor to the previous occurrence of search pattern match,
scrolling up/down through the content to display the cursor position.
When called the first time, it moves the cursor to the last match,
subsequent calls move the cursor backwards through the matches. When
the cursor is at the first match and this method is called, the cursor
will be moved back to the last match
If there are no matches, this method does nothing.
"""
if not self._matches:
return
if self._current_match < 0:
self._current_match = len(self._matches) - 1
self._move_to_match(self._matches[self._current_match][0],
self._matches[self._current_match][1])
self._current_match -= 1
def _move_to_match(self, pos, length):
"""
Moves the cursor in the content box to the given pos, then moves it
forwards by "length" steps, selecting the characters in between
@param pos: The starting position to move the cursor to
@type pos: int
@param length: The number of steps to move+select after the starting
index
@type length: int
@postcondition: The cursor is moved to pos, the characters between pos
and length are selected, and the content is scrolled
up/down to ensure the cursor is visible
"""
self._cursor.setPosition(pos)
self._cursor.movePosition(QtGui.QTextCursor.Right,
QtGui.QTextCursor.KeepAnchor,
length)
self.setTextCursor(self._cursor)
self.ensureCursorVisible()
#self._scrollbar_value = self._log_scrollbar.value()
self._highlight_matches()
self._matches_label.setText('%d:%d matches'
% (self._current_match + 1,
len(self._matches)))
def _find_text(self, pattern:str, flags:int, isRegexPattern:bool):
"""
Finds and stores the list of text fragments matching the search pattern
entered in the search box.
@postcondition: The text matching the search pattern is stored for
later access & processing
"""
prev_search = self._current_search
self._current_search = (pattern, flags, isRegexPattern)
search_has_changed = (
self._current_search[0] != prev_search[0] or
self._current_search[1] != prev_search[1] or
self._current_search[2] != prev_search[2]
)
if not self._current_search[0]: # Nothing to search for, clear search data
self._clear_search_data()
return
if self._content_timestamp <= self._search_timestamp and not search_has_changed:
self._move_to_next_match()
return
# New Search
self._clear_search_data()
try:
match_objects = re.finditer(str(pattern), self.toPlainText(), flags)
for match in match_objects:
index = match.start()
length = len(match.group(0))
self._matches.append((index, length))
if not self._matches:
self._matches_label.setStyleSheet('QLabel {color : gray}')
self._matches_label.setText('No Matches Found')
self._matches_to_highlight = set(self._matches)
self._update_visible_indices()
self._highlight_matches()
self._search_timestamp = time.time()
# Start navigating
self._current_match = 0
self._move_to_next_match()
except re.error as err:
self._matches_label.setText('ERROR: %s' % str(err))
self._matches_label.setStyleSheet('QLabel {color : indianred}')
def _highlight_matches(self):
"""
Highlights the matches closest to the current match
(current = the one the cursor is at)
(closest = up to 300 matches before + up to 300 matches after)
@postcondition: The matches closest to the current match have a new
background color (Red)
"""
if not self._matches_to_highlight or not self._matches:
return # nothing to match
# Update matches around the current one (300 before and 300 after)
highlight = self._matches[max(self._current_match - 300, 0):
min(self._current_match + 300, len(self._matches))]
matches = list(set(highlight).intersection(self._matches_to_highlight))
for match in matches:
self._highlight_cursor.setPosition(match[0])
self._highlight_cursor.movePosition(QtGui.QTextCursor.Right,
QtGui.QTextCursor.KeepAnchor,
match[-1])
self._highlight_cursor.setCharFormat(self._format)
self._matches_to_highlight.discard(match)
def _clear_search_data(self):
"""
Removes the text in the search pattern box, clears all highlights and
stored search data
@postcondition: The text in the search field is removed, match list is
cleared, and format/selection in the main content box
are also removed.
"""
self._matches = []
self._matches_to_highlight = set()
self._search_timestamp = 0
self._matches_label.setText('')
format = QtGui.QTextCharFormat()
self._highlight_cursor.setPosition(QtGui.QTextCursor.Start)
self._highlight_cursor.movePosition(QtGui.QTextCursor.End, mode=QtGui.QTextCursor.KeepAnchor)
self._highlight_cursor.setCharFormat(format)
self._highlight_cursor.clearSelection()
def _update_visible_indices(self):
"""
Updates the stored first & last visible text content indices so we
can focus operations like highlighting on text that is visible
@postcondition: The _first_visible_index & _last_visible_index are
up to date (in sync with the current viewport)
"""
viewport = self.viewport()
try:
top_left = QtCore.QPoint(0, 0)
bottom_right = QtCore.QPoint(viewport.width() - 1,
viewport.height() - 1)
first = self.cursorForPosition(top_left).position()
last = self.cursorForPosition(bottom_right).position()
self._first_visible_index = first
self._last_visible_index = last
except IndexError: # When there's nothing in the content box
pass
编辑:使用 QSyntaxHightlighter 是一种仅突出显示的优雅方法,但是我必须使用 manuel 方法有两个原因 a) 日志文件可能会变得很重,因此上述解决方案允许我们仅在可见行范围内限制突出显示 b)我必须能够在文档中的匹配项之间跳转