0

例子

我构建了一个自定义的 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)我必须能够在文档中的匹配项之间跳转

4

0 回答 0