You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

PySide6 GUI中用RichHandler+QTextEdit显示HTML日志遇间距对齐问题

问题:PySide6 QTextEdit结合RichHandler显示日志行间距过大、对齐异常

我在PySide6的GUI中用自定义QTextEditLogger类(继承QTextEditlogging.Handler)展示应用日志,纯文本插入时显示正常,但用rich.RichHandler格式化彩色日志并以HTML插入时,颜色正常但行间距过大、首行缩进异常(已关闭自动换行)。

纯文本正常代码:

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)

最小可复现代码:

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_())

解决方案

问题根源是console.export_html()会输出包含完整HTML文档结构(含<body><p>标签)的内容,每次调用insertHtml()插入这些完整结构,会导致QTextEdit解析时产生额外的边距和换行,进而出现行间距过大、首行缩进异常。

修改思路:提取日志内容的核心HTML片段,去掉冗余的外层标签,手动添加换行。

完整修改后的代码

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)
        
        # 提取body内的核心内容,去除外层p标签
        start_idx = html.find('<body>') + len('<body>')
        end_idx = html.find('</body>')
        core_content = html[start_idx:end_idx].strip()
        core_content = core_content.replace('<p>', '').replace('</p>', '')
        
        # 插入处理后的HTML,手动添加换行
        self.insertHtml(core_content + '<br>')
        
        # 滚动到底部
        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_())

说明

  • 通过提取<body>标签内的内容,去掉默认包裹日志的<p>标签,避免了QTextEdit解析时产生的默认边距和缩进。
  • 手动添加<br>标签实现日志行的换行,保证行间距与纯文本模式一致。
  • 保留了RichHandler的彩色格式化效果,同时解决了布局异常问题。

内容的提问来源于stack exchange,提问作者Raphael

火山引擎 最新活动