Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. How to make QTextDocument highlight text without repositioning it?
QtWS25 Last Chance

How to make QTextDocument highlight text without repositioning it?

Scheduled Pinned Locked Moved Unsolved General and Desktop
2 Posts 1 Posters 414 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • H Offline
    H Offline
    HForest
    wrote on 19 Jul 2022, 07:03 last edited by HForest
    #1

    I have a bunch of items (forum posts) in a view, and a delegate that uses a QTextDocument to render & hit-test the rich text in each item. When the user selects text, I use QTextCursor.setCharFormat() to apply selected-text colors to the appropriate characters.

    The problem:

    When the start or end of a selection lands between certain character pairs, QTextDocument repositions the rest of the characters on the line. (It looks like kerning gets disabled for characters whose colors don't match.) The shift is slight, but noticeable, and sometimes even wraps the last word on a line down to the next line. The result is text that jitters back and forth while the user is selecting it.

    These sample images show the problem. They are very similar, but if you use an image viewer to switch back and forth between the first and third image, or the second and third one, the text repositioning is noticeable:

    1. No highlighting:
      highlight-none.jpg

    2. Highlighting with QTextEdit + setTextCursor() works nicely:
      highlight-qedit.jpg

    3. Highlighting with QTextDocument + setCharFormat() shifts text slightly to the right: (Compare this with image 1 or 2.)
      highlight-qtextdocument.jpg

    4. Edit to add: example of QTextDocument text shift causing words to re-wrap: (Compare this with image 1.)
      highlight-wordwrap.jpg

    I know Qt has some way to avoid this problem, since widgets like QTextEdit and QLabel can highlight selected text without moving it at all. (See image 2.) How can I do the same with QTextDocument?

    An obvious workaround would be to use QTextEdit instead of QTextDocument, but my view will contain thousands of items, and I have read that performance would suffer with that many widgets.

    Here's a program that can render & highlight text using different strategies, to demonstrate the problem:

    #!/usr/bin/env python3
    
    import signal
    import sys
    
    from PySide2.QtCore import QPoint, QRectF, Qt
    from PySide2.QtGui import QKeySequence, QPainter, QTextCursor, QSyntaxHighlighter, QTextDocument
    from PySide2.QtWidgets import QApplication, QFrame, QShortcut, QTextEdit, QWidget
    
    
    TEXT = """# ToovevoV.t'rrorqre To V. To V. To V. adipiscing word adipiscing
    
    ---
    This program demonstrates how QTextDocument repositions text when
    highlighting is applied to part of a word, starting or ending between
    certain adjacent letters.
    
    Use the mouse wheel or arrow keys to apply text highlighting. Watch for
    text shifting horizontally when the highlight ends within a word. These shifts
    are usually very slight, but are more pronounced between glyph pairs like "V."
    or "To". Due to word wrap, these shifts can even push a line's last word down
    to the next line, depending on word length and window width.
    
    Meanwhile, QTextEdit and QLabel apply highlighting without shifting the text.
    Run this program with the "edit" command line argument to try it.
    """
    
    
    class TextEditRenderer(QTextEdit):
    
        STRATEGY = "QTextEdit + setTextCursor()"
    
        def __init__(self, width):
            super().__init__()
    
            self.setMarkdown(TEXT)
    
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.setFrameStyle(QFrame.NoFrame)
    
            self.setAttribute(Qt.WA_DontShowOnScreen)
            self.show()
            self.setFixedWidth(width)
            self.setFixedHeight(self.document().size().height())
    
        def get_size(self):
            return self.size()
    
        def calculate_cursor_pos(self, oldpos, cursorop):
            cursor = self.textCursor()
            cursor.setPosition(oldpos)
            cursor.movePosition(cursorop)
            return cursor.position()
    
        def paint(self, painter, palette, selectlen):
            cursor = self.textCursor()
            cursor.setPosition(selectlen, QTextCursor.KeepAnchor)
            self.setTextCursor(cursor)
    
            self.render(painter, QPoint())
    
    
    class TextDocumentRenderer(QTextDocument):
    
        STRATEGY = "QTextDocument + setCharFormat()"
    
        def __init__(self, width):
            super().__init__()
            self.setMarkdown(TEXT)
            self.setTextWidth(width)
    
        def get_size(self):
            return self.size().toSize()
    
        def calculate_cursor_pos(self, oldpos, cursorop):
            cursor = QTextCursor(self)
            cursor.setPosition(oldpos)
            cursor.movePosition(cursorop)
            return cursor.position()
    
        def paint(self, painter, palette, selectlen):
            cursor = QTextCursor(self)
            cursor.setPosition(selectlen, QTextCursor.KeepAnchor)
    
            cformat = cursor.charFormat()
            cformat.setBackground(palette.highlight())
            cformat.setForeground(palette.highlightedText())
            cursor.setCharFormat(cformat)
    
            rect = QRectF(QPoint(), self.size())
            painter.fillRect(rect, palette.base())
            self.drawContents(painter)
    
    
    class Highlighter(QSyntaxHighlighter):
    
        def __init__(self, parent, palette, selectlen):
            super().__init__(parent)
            self.selectlen = selectlen
            self.palette = palette
    
        def highlightBlock(self, text):
            offset = max(self.previousBlockState(), 0)
            remain = self.selectlen - offset
            count = min(remain, len(text))
            if remain > 0:
                cformat = self.format(0)
                cformat.setBackground(self.palette.highlight())
                cformat.setForeground(self.palette.highlightedText())
                self.setFormat(0, count, cformat)
            self.setCurrentBlockState(offset + len(text))
    
    
    class SyntaxHighlighterRenderer(TextDocumentRenderer):
    
        STRATEGY = "QTextDocument + QSyntaxHighlighter"
    
        def paint(self, painter, palette, selectlen):
            highlighter = Highlighter(self, palette, selectlen)
            highlighter.rehighlight()
    
            rect = QRectF(QPoint(), self.size())
            painter.fillRect(rect, palette.base())
            self.drawContents(painter)
    
    
    class MainWidget(QWidget):
    
        def __init__(self, rendertype, width):
            super().__init__()
            self.rendertype = rendertype
            renderer = self.rendertype(width)
            self.resize(renderer.get_size())
    
            self.selectlen = 0  # number of characters to highlight
    
            quit_shortcut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self)
            quit_shortcut.activated.connect(QApplication.quit)
    
        def resizeEvent(self, event):
            self.window().setWindowTitle(
                f"{event.size().width()}x{event.size().height()}")
    
        def sizeHint(self):
            renderer = self.rendertype(self.width())
            return renderer.get_size()
    
        def paintEvent(self, event):
            renderer = self.rendertype(self.width())
            renderer.paint(QPainter(self), self.palette(), self.selectlen)
    
        def wheelEvent(self, event):
            self._change_selection(event, 1 if event.angleDelta().y() > 0 else 0)
    
        def keyPressEvent(self, event):
            if event.key() in (Qt.Key_Right, Qt.Key_Up):
                self._change_selection(event, 1)
            elif event.key() in (Qt.Key_Left, Qt.Key_Down):
                self._change_selection(event, 0)
    
        CHAROPS = (QTextCursor.PreviousCharacter, QTextCursor.NextCharacter)
        WORDOPS = (QTextCursor.PreviousWord, QTextCursor.NextWord)
    
        def _change_selection(self, event, direction):
    
            operations = self.WORDOPS if event.modifiers() else self.CHAROPS
            cursorop = operations[direction]
    
            renderer = self.rendertype(self.width())
            self.selectlen = renderer.calculate_cursor_pos(self.selectlen, cursorop)
    
            self.update()
    
    
    def main():
    
        if len(sys.argv) < 2:
            rendertype = TextDocumentRenderer
        elif sys.argv[1].startswith('h'):
            rendertype = SyntaxHighlighterRenderer
        else:
            rendertype = TextEditRenderer
        print("highlighting strategy:", rendertype.STRATEGY)
    
        app = QApplication(sys.argv)
        signal.signal(signal.SIGINT, signal.SIG_DFL)
    
        # I chose an initial window width that makes the problem obvious on my
        # system, due to its effect on word wrap. People with different desktop
        # and font settings might need a different width to see that effect.
        widget = MainWidget(rendertype, width=343)
        widget.show()
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()
    
    
    1 Reply Last reply
    0
    • H Offline
      H Offline
      HForest
      wrote on 20 Jul 2022, 21:26 last edited by HForest
      #2

      @SGaist my question is about Qt's internal behavior, not about Python or the Python bindings. (I used python in some sample code for the sake of simplicity; a C++ version would surely have the same behavior.) I believe it is therefore unlikely to get any answers in the Python forum. Would you please move it back?

      1 Reply Last reply
      0

      1/2

      19 Jul 2022, 07:03

      • Login

      • Login or register to search.
      1 out of 2
      • First post
        1/2
        Last post
      0
      • Categories
      • Recent
      • Tags
      • Popular
      • Users
      • Groups
      • Search
      • Get Qt Extensions
      • Unsolved