PDA

View Full Version : Apply the outline of a QTextCharFormat only on the outside of the text



Sylve
4th April 2021, 20:03
Hello everyone,

I have been using Qt for a personal project and I am so far pleasantly surprised how powerful it is.

One issue I am still struggling with despite several hours of digging is the following. I want to write text inside a QTextEdit with an outline around the text. I manage to do it with the following code, however there is an apparent problem (see the picture below): the outline is drawn not only outside the text, but also inside.


from PyQt5.QtWidgets import *
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPen, QFont
from PyQt5.QtGui import QTextCursor

class MyTextEdit(QTextEdit):
def __init__(self):
super().__init__()

self.resize(800, 400)

font = self.currentFont()
font.setPointSize(80)
font.setStyleStrategy(QFont.PreferAntialias)
self.setFont(font)

my_cursor = self.textCursor()
my_cursor.movePosition(QTextCursor.Start)

my_char_format = my_cursor.charFormat()
my_cursor.setCharFormat(my_char_format)

my_cursor.insertText("Text example", my_char_format)

outline_format = my_char_format
outline_format.setTextOutline(QPen(Qt.red, 5))

my_cursor.setCharFormat(outline_format)
my_cursor.insertBlock()
my_cursor.insertText("Text example")


if __name__ == '__main__':
app = QApplication([sys.argv])
win = MyTextEdit()

win.show()

app.exec_()

Result:
https://i.imgur.com/cFnU3AM.png

This problem has already been encountered, for example here: https://stackoverflow.com/questions/13966868/qt-outlined-text-without-thinning-font and here https://stackoverflow.com/questions/15166754/stroking-a-path-only-inside-outside .

However, I do not understand the answer given using the print() method, which is not available for QTextEdit, although it is available for QGraphicsTextItem.

I have a hard constraint that the text should still be selectable, and nicely selectable. I do not need the text to be editable.

I wonder if, for example, overriding the paintEvent function would allow me to do more advanced text painting (for example, writing first the text with outline, and then overriding with the text without outline but keeping the first painting under it), but still having the text selectable.

Thanks a lot for your help, I will still be thinking meanwhile and post if I find a solution :)

Edit 2:

I think I got it. I should implement a class inheriting from QTextDocument, and overwrite its print() method as detailed in the Stackoverflow answer.

Then, for my custom QTextEdit in my init, I should use setDocument() with my custom QTextDocument class where print() is overwritten.

See https://doc.qt.io/qt-5/qtextedit.html#document-prop

I will try that later and let you know if this works.

Edit:

Here is an example where the outline gets indeed only outside the text, however the text is not selectable anymore:

from PyQt5.QtWidgets import *
import sys
from PyQt5 import QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QPainter, QPainterPath, QBrush, QPen, QPalette, QFont

class MyTextEdit(QTextEdit):
def __init__(self, parent = None):
super(MyTextEdit, self).__init__(parent)

self.resize(800, 400)

def paintEvent(self, event):
painter = QPainter(self.viewport())
painter.setRenderHint(QPainter.Antialiasing);

painter.drawLine(10, 10, 200, 10) # just for testing purposes

#font = self.font()
font = QFont("Arial", 72, 50, False);

text = "Text example"
text_path = QPainterPath()

text_path.addText(0, 100, font, text)

painter.setFont(font)

# draw outline
painter.setPen(outline_pen)
painter.drawPath(text_path)

# draw text
color = self.palette().color(QPalette.Text)
painter.setPen(color)
painter.drawText(0, 100, text)

super(MyTextEdit, self).paintEvent(event)

if __name__ == '__main__':
app = QApplication([sys.argv])

outline_color = QColor(200, 0, 0, 180)
outline_brush = QBrush(outline_color, Qt.SolidPattern)
outline_pen = QPen(outline_brush, 15, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)

win = MyTextEdit()

win.show()

app.exec_()

Sylve
5th April 2021, 12:56
Unfortunately I think my second edit can not work, I have mistaken print() and paint() functions. QTextDocument has no such paint().

Sylve
5th April 2021, 15:11
I post my solution (in the end heavily based on the Stackoverflow one), just in case it helps anybody. Note I used Qt 6 intead of 5, but it should not matter.


from PyQt6.QtWidgets import *
import sys
from PyQt6 import QtGui
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QTextDocument, QTextCursor
from PyQt6.QtGui import QColor, QPainter, QPainterPath, QBrush, QPen, QPalette, QFont

class MyTextDocument(QTextDocument):
def __init__(self, parent, font):
super().__init__()

self.parent = parent

self.setDefaultFont(font)

def drawContents(self, painter):

super().drawContents(painter)

my_cursor = self.parent.textCursor()
my_char_format = my_cursor.charFormat()

my_char_format.setTextOutline(QPen(Qt.GlobalColor. red, 5))
my_cursor.select(QTextCursor.SelectionType.Documen t)
my_cursor.mergeCharFormat(my_char_format)

super().drawContents(painter)

my_char_format.setTextOutline(QPen(Qt.GlobalColor. transparent))
my_cursor.mergeCharFormat(my_char_format)

super().drawContents(painter)


class MyTextEdit(QTextEdit):
def __init__(self):
super().__init__()

font = self.currentFont()
font.setPointSize(80)
font.setStyleStrategy(QFont.StyleStrategy.PreferAn tialias)

self.setDocument(MyTextDocument(self, font))

self.setText("Does it work?")
self.resize(800, 400)

def paintEvent(self, event):
painter = QPainter(self.viewport())

super(MyTextEdit, self).paintEvent(event)

self.document().drawContents(painter)


if __name__ == '__main__':
app = QApplication(sys.argv)

win = MyTextEdit()
win.show()

app.exec_()

Result:

https://i.imgur.com/1vCis5b.png

Feel free to post if you have something better in mind / any comment!

Sylve
5th April 2021, 18:30
I found a very concerning issue with this solution: the line


my_cursor.select(QTextCursor.SelectionType.Documen t)

calls recursively paintEvent in a infinite loop. I should find a way to disable recursive calls in paintEvent...

Try out this code to see the issue:


from PyQt5.QtWidgets import *
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextDocument, QTextCursor
from PyQt5.QtGui import QPainter, QPen, QFont

style_subs = '''
/* looks of subtitles */
QFrame {
background: green;
color: white;
font-family: "Noto Serif CJK JP Light";
border: 0;
}
'''

class MyTextEdit(QTextEdit):
def __init__(self):
super().__init__()

self.setStyleSheet(style_subs)

font = self.currentFont()
font.setPointSize(80)
font.setStyleStrategy(QFont.PreferAntialias)
self.setFont(font)

self.setText("Does it work?")

self.resize(800, 400)

self.n = 0

def paintEvent(self, event):
print("painting", self.n, "...")
self.n += 1
painter = QPainter(self.viewport())

my_cursor = self.textCursor()
my_char_format = my_cursor.charFormat()

my_char_format.setTextOutline(QPen(Qt.red, 10))
my_cursor.select(QTextCursor.SelectionType.Documen t)
my_cursor.mergeCharFormat(my_char_format)

self.document().drawContents(painter)

my_char_format.setTextOutline(QPen(Qt.transparent) )
my_cursor.mergeCharFormat(my_char_format)


super(MyTextEdit, self).paintEvent(event)


if __name__ == '__main__':
app = QApplication(sys.argv)

win = MyTextEdit()
win.show()

app.exec_()