I want to show the application log on the GUI in some way.
I am using my own class, which inherits from both QTextEdit and logging.Handler. It is added to logging as a handler at init.
If I insert the text as plaintext into the widget, it prints fine. Linespacing is just fine. For that i simply do
class QTextEditLogger(QTextEdit, logging.Handler):
def __init__(self, parent, level=NOTSET):
QTextEdit.__init__(self, parent)
logging.Handler.__init(self, level=level)
def emit(self, record):
msg = self.format(record)
self.append(msg)
When I try to use rich.RichHandler formatting to get colored text and insert that as HTML into the QTextEdit, the color works fine; however, the text is not properly spaced /aligned.
As you can the the line spaicng is too large, and word wrap looks really weird, even though that is turned off for the widget and in the console.
Here is the MRE, I add a RichHandler as member to my class and a Console object that logs to os.devnull and records.
I overloaded the emit function, when the Handler gets called I first dispatch the RichHandler emit function. Then I grab the console text via export_html(), which in turn is added to the textedit via insertHTML(). I even tried to set the console size to the widget width, but that didnt really help.
How can I remove the space between the lines and fix the indent from the first line?
import sys
import os
from time import sleep
import logging
from logging import Handler, NOTSET
from rich.logging import RichHandler
from rich.console import Console
from PySide6.QtWidgets import QTextEdit, QApplication, QMainWindow
from PySide6.QtGui import QTextCursor, QTextOption
class QTextEditLogger(QTextEdit, Handler):
"""A QTextEdit logger that uses RichHandler to format log messages."""
def __init__(self, parent=None, level=NOTSET):
QTextEdit.__init__(self, parent)
Handler.__init__(self,level=level)
self.console = Console(file=open(os.devnull, "wt"), record=True,width=42, height=12, soft_wrap=False)
self.rich_handler = RichHandler(show_time=False, show_path=False, show_level=True, markup=True, console=self.console, log_time_format="[%X]", level=self.level)
self.rich_handler.setLevel(self.level)
QTextEdit.setWordWrapMode(self, QTextOption.WrapMode.NoWrap)
self.setAcceptRichText(True)
self.setReadOnly(True)
def showEvent(self, arg__1):
self.console.width = self.width()//self.fontMetrics().averageCharWidth() # Approximate character width
self.console.height = self.height()//self.fontMetrics().height() # Approximate character height
return super().showEvent(arg__1)
def emit(self, record) -> None:
"""Override the emit method to handle log records."""
self.rich_handler.emit(record)
html = self.console.export_html(clear=True, inline_styles=True)
self.insertHtml(html)
self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().maximum())
c = self.textCursor()
c.movePosition(QTextCursor.End)
self.setTextCursor(c)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QTextEdit Example")
# Create a QTextEdit widget
self.text_edit = QTextEditLogger(self)
self.setCentralWidget(self.text_edit)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
# Set up logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.addHandler(window.text_edit)
logger.info("This is an info message.")
sleep(.5)
logger.warning("This is a warning message.")
sleep(.5)
for i in range(10):
logger.debug(f"This is a debug message {i}.")
logger.error("This is an error message.")
sys.exit(app.exec_())
Thanks to @musicamante, I realized I had to supply my own html template to export_html. That was not immediately clear to me as its named a bit weird.
Not only was I injecting entire HTML pages into the TextEdit, but also the <pre> tags that are used by default cause the spacing issues, so i used <div> instead.
I also set the QTextEdit to use the monospace font that would normally be selected in the HTML stylesheet.
EDIT: I updated the solution with a better html template that also supports the original spacing/alignment of the RichLogger and indent at word wrap. This is what ended up working:
class QTextEditLogger(QTextEdit, Handler):
"""A QTextEdit logger that uses RichHandler to format log messages."""
def __init__(self, parent=None, level=NOTSET):
QTextEdit.__init__(self, parent)
Handler.__init__(self,level=level)
self.console = Console(file=open(os.devnull, "wt"), record=True, width=100, height=12, soft_wrap=False, color_system="truecolor", tab_size=4)
self.rich_handler = RichHandler(show_time=False, show_path=False, show_level=True, markup=True, console=self.console, level=self.level)
self.rich_handler.setLevel(self.level)
self.setWordWrapMode(QTextOption.WrapMode.WordWrap)
self.setAcceptRichText(True)
self.setReadOnly(True)
font = QFont(['Menlo','DejaVu Sans Mono','consolas','Courier New','monospace'], 10, self.font().weight())
font.setStyleHint(QFont.StyleHint.TypeWriter)
self.setFont(font)
def emit(self, record) -> None:
"""Override the emit method to handle log records."""
self.rich_handler.emit(record)
indent_width = 11*self.fontMetrics().averageCharWidth()
html_template = f'<p style="background-color: {{background}}; color: {{foreground}}; margin: 0; margin-left:{indent_width}px; text-indent:-{indent_width}px;white-space: pre-wrap"><code>{{code}}</code></p>'
html = self.console.export_html(clear=True, code_format=html_template, inline_styles=True)
# remove padding at end of string, since that stems from console width wich is invalid here
html = re.sub(r'\s+[\n]', '\n', html)
self.insertHtml(html)
self.verticalScrollBar().setSliderPosition(self.verticalScrollBar().maximum())
c = self.textCursor()
c.movePosition(QTextCursor.MoveOperation.End)
self.setTextCursor(c)