我构建了一个自定义的PlainTextEdit作为logviewer小部件。我有一个外部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)我必须能够在文档中的匹配项之间跳转