Issue
I have a problem which is very similar to this topic. Sadly it did not get fully solved back then.
I have a custom QTextEdit
widget in my PyQt application. I've added few features to it to make it fancier. Especially, I lately added a feature which, when a line is selected, uses setBlockFormat
to color the line.
But swapping the format pushes the command to the QTextEdit
undo stack, and I don't want to trigger this behaviour.
The previous topic did not get a proper answer. The "easier" way on paper would be to block it. However it does not seem to be possible to do that, is it?
The other (and harder) way would be to set the QTextEdit with a custom QUndoStack
, which would only pushes key event, but I'm a bit lost with this solution, especially with the deletion of characters (basically I would want to have only typed/deleted text in the Undo stack).
Am I mistaken in my understanding of the topic? Is there a simple solution that I don't see?
EDIT: to answer the question, the purpose of highlighting the line is just to emphasize current line selection, nothing more. It's what is done in software such as Notepad++. Attached is a screenshot of the feature.
Solution
Visually highlighting a portion of text should never directly affect the underlying QTextDocument.
While it's probably possible to achieve this by using a smart implementation of QSyntaxHighlighter (through overriding of highlightBlock()
and by setting block states), if you already have a syntax highlighter with custom states and user data, supporting this might become quite a handful.
Also, by trying to change the background color of text, the result is that any other highlighting that actually changes the background becomes invisible in the selection.
I propose another solution: overriding the text edit paintEvent()
and use the text cursor selection to draw any highlighting before the default painting.
The procedure is the following:
- create a list for the highlighted blocks;
- connect the
selectionChanged
signal of the QTextEdit to a function that will:- create a temporary list and check if any selection exists;
- cycle through each block of the document, and if the selection is contained in a block, add it to the list;
- if the default list is different from the temporary one, replace it and schedule an
update()
on theviewport()
(important! CallingtextEdit.update()
is a no-op, since you need to update the contents of its scroll area)
- in the
paintEvent()
override, check if a "highlighted" list of blocks exists, and then:- query the
documentLayout()
; - create an empty QRect;
- iterate all "highlighted" blocks and get their
blockBoundingRect()
that will be merged with the QRect above; - create a QPainter on the viewport and draw the resulting QRect;
- query the
- call the basic implementation;
Now, this might be not very effective, especially starting from point 3.
and considering that QTextEdit calls paintEvent()
every time the cursor "caret" blinks. The solution is to "cache" the highlighted rectangle using QPicture, so that you don't need to compute everything at each paint event. The only requirement is to clear that cache whenever the text edit is resized, since the layout might change the bounding rect of each block.
Here is the result (using NoWrap
to show the result with scrolling):
...and the code:
class HighlightTextEdit(QTextEdit):
highlightCache = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.highlightBlocks = []
self.selectionChanged.connect(self.highlightSelection)
def highlightSelection(self):
cursor = self.textCursor()
highlightBlocks = []
if cursor.hasSelection():
selectionStart = cursor.selectionStart()
selectionEnd = cursor.selectionEnd()
block = self.document().begin()
while block.isValid():
blockStart = block.position()
blockEnd = blockStart + block.length()
if blockEnd > selectionStart:
# the block probably contains the selection
if blockStart > selectionEnd:
# the block begins before the end of the selection:
# we can ignore it and any other block after it
break
highlightBlocks.append(block)
block = block.next()
if self.highlightBlocks != highlightBlocks:
self.highlightBlocks = highlightBlocks
self.highlightCache = None
self.viewport().update()
def updateHighlightCache(self):
if not self.highlightBlocks:
self.highlightCache = None
return
docLayout = self.document().documentLayout()
rect = QRectF()
for block in self.highlightBlocks:
rect |= docLayout.blockBoundingRect(block)
self.highlightCache = QPicture()
qp = QPainter(self.highlightCache)
qp.setPen(Qt.NoPen)
color = self.palette().color(QPalette.Highlight)
color.setAlphaF(max(.5, color.alphaF() * .5))
qp.setBrush(color)
qp.drawRect(rect)
def paintEvent(self, event):
if self.highlightBlocks:
if self.highlightCache is None:
self.updateHighlightCache()
qp = QPainter(self.viewport())
# translate according to the scroll bars
qp.translate(
-self.horizontalScrollBar().value(),
-self.verticalScrollBar().value()
)
self.highlightCache.play(qp)
super().paintEvent(event)
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateHighlightCache()
Note that the above is quite effective, since you can do whatever you want with the given bounding rects. For instance, instead of just drawing a merged rectangle, you could create "expanded" rects of each block and add them to a QPainterPath as rounded rects:
def updateHighlightCache(self):
if not self.highlightBlocks:
self.highlightCache = None
return
docLayout = self.document().documentLayout()
path = QPainterPath()
path.setFillRule(Qt.WindingFill)
for block in self.highlightBlocks:
rect = docLayout.blockBoundingRect(block)
path.addRoundedRect(rect.adjusted(-1, -1, 1, 1), 2, 2)
self.highlightCache = QPicture()
qp = QPainter(self.highlightCache)
qp.setPen(Qt.NoPen)
color = self.palette().color(QPalette.Highlight)
color.setAlphaF(max(.5, color.alphaF() * .5))
qp.setBrush(color)
qp.drawPath(path)
Another important aspect is that if the QTextDocument contains rich text with laid out items (such as table cells or inner frames), the selection might only include the rectangle of that block. In that case, if you still want to highlight the horizontal bounding rect, you need to merge the rectangle of the bounding rects with the rectangle of the whole document (the "top frame"), while considering the document margins:
def updateHighlightCache(self):
if not self.highlightBlocks:
self.highlightCache = None
return
docLayout = self.document().documentLayout()
rect = QRectF()
for block in self.highlightBlocks:
rect |= docLayout.blockBoundingRect(block)
topRect = docLayout.frameBoundingRect(self.document().rootFrame())
margin = self.document().documentMargin()
rect.setLeft(topRect.x() + margin)
rect.setRight(min(rect.right(), topRect.right() - margin * 2))
self.highlightCache = QPicture()
qp = QPainter(self.highlightCache)
qp.setPen(Qt.NoPen)
color = self.palette().color(QPalette.Highlight)
color.setAlphaF(max(.5, color.alphaF() * .5))
qp.setBrush(color)
qp.drawRect(rect)
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.