PyQt6 优化操作:将运行日志以及程序崩溃时的报错信息显示到界面控件

1. 总体目标

1)将 log 打印和 print 打印的内容输出到界面控件,不同 log 级别对应不同颜色;

如果不需要输出print 内容,注释掉第73、74行(如下示例),以及 onUpdateText 函数即可。

# sys.stdout = Stream()
# sys.stdout.newText.connect(self.onUpdateText)

2)log内容输出到本地文件

3)程序遇到异常时不会立刻退出,弹窗显示报错内容,并提供选项退出程序或继续运行

2. 知识点

1.1 Python logging 模块

Handler处理器:在 Python 的 logging 模块中,Handler 是用来处理日志记录的对象。它负责将日志记录发送到指定的目标,例如文件、终端或者网络。Handler 可以被添加到 Logger 对象中,以便将记录传递给它们。

如需更多信息,请查阅参考链接1,此文对 logging 模块进行了详细的介绍。

1.2 Qt 消息处理

qInstallMessageHandler:qInstallMessageHandler 是 PyQt 中的一个函数,用于管理 Qt 应用程序中的消息。使用时,需要设置Qt消息处理程序。一旦设置,所有 Qt 消息都将由该处理程序处理。通过这个函数,可将 Qt 框架中的所有消息(如警告、错误、调试信息等)重定向到自定义的消息处理函数中,例如将消息输出到控制台、写入日志文件等。

如需更多信息,请查阅参考链接2,此文内容清晰,给博主点赞,PyQt6需稍作改动,参考本文。

1.3 sys.excepthook

当程序中出现未处理的异常时,Python 解释器会调用 sys.excepthook 函数来处理异常。默认情况下,sys.excepthook 函数会将异常信息输出到标准错误流 (sys.stderr) 中,然后退出程序。但是,我们可以通过设置 sys.excepthook 函数来定制自己的异常处理方式,例如记录日志、弹窗提示等。

当Python 程序遇到错误时,它会生成一个 traceback,其中包含了错误发生的位置、错误类型和错误消息等信息。Traceback 通常是在 Python 解释器中显示的,它可以帮助程序员快速定位错误,并且可以提供有用的调试信息。Traceback 可以通过使用 Python 内置的 traceback 模块来捕获和处理。

3. 代码分享

# -*- coding: utf-8 -*-、
import logging
import inspect
import traceback
import os
import sys
from datetime import datetime
from random import randint
from PyQt6.QtCore import pyqtSignal, QObject, QtMsgType, qInstallMessageHandler
from PyQt6.QtWidgets import QDialog, QApplication, QTextEdit, QPushButton, QMessageBox


class Stream(QObject):
    """
    重定向控制台输出到文本框控件
    """
    newText = pyqtSignal(str)

    # 任何定义了类似于文件write方法的对象可以指定给sys.stdout,
    # 所有的标准输出将发送到该方法对象上
    def write(self, text):
        self.newText.emit(str(text))
        QApplication.processEvents()


class QTextEditHandler(logging.Handler):
    def __init__(self, parent):
        super().__init__()
        self.text_edit = parent
        self.formatter = logging.Formatter('%(asctime)s - [%(filename)s][line:%(lineno)d][%(levelname)s]  %(message)s')

    def emit(self, record):
        msg = self.format(record)
        self.text_edit.append(msg)
        self.text_edit.ensureCursorVisible()

    def format(self, record):
        if record.levelno == logging.DEBUG:
            color = 'gray'
        elif record.levelno == logging.INFO:
            color = 'black'
        elif record.levelno == logging.WARNING:
            color = 'orange'
        elif record.levelno == logging.ERROR:
            color = 'darkRed'
        elif record.levelno == logging.CRITICAL or record.levelno == logging.FATAL:
            color = 'red'
        else:
            color = 'blue'
        new_msg = self.formatter.format(record)
        msg = '<span style="color:{}">{}</span>'.format(color, new_msg)
        return msg


class Ui_Form(QDialog):
    """
    主界面
    """

    def __init__(self):
        super().__init__()
        self.resize(1000, 500)
        self.textEdit = QTextEdit(self)
        self.textEdit.resize(900, 200)
        self.QPushButton1 = QPushButton('点我运行正常程序', self)
        self.QPushButton1.clicked.connect(self.normal_func)
        self.QPushButton1.move(10, 250)
        self.QPushButton2 = QPushButton('点我运行异常程序', self)
        self.QPushButton2.clicked.connect(self.error_func)
        self.QPushButton2.move(10, 280)

        # 自定义输出流
        sys.stdout = Stream()
        sys.stdout.newText.connect(self.onUpdateText)

        # Log打印
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.DEBUG)
        formatter = logging.Formatter('%(asctime)s - [%(filename)s][line:%(lineno)d][%(levelname)s]  %(message)s')
        # QTextEdit-Handler
        handler = QTextEditHandler(self.textEdit)
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)
        # File-Handler
        log_file_name = os.path.join(os.path.dirname(__file__), 'Log_Folder', 'log_{time}_{randomn}'.format(
            time=datetime.now().strftime('%Y_%m%d_%H%M%S'), randomn=randint(0, 9)))
        os.makedirs(os.path.dirname(log_file_name), exist_ok=True)
        file_handler = logging.FileHandler(log_file_name)
        file_handler.setFormatter(formatter)
        self.logger.addHandler(file_handler)

        # QT 消息处理
        qInstallMessageHandler(self.redirection_msg)

    def closeEvent(self, event):
        """
        重写closeEvent, 程序结束时将stdout恢复默认
        """
        reply = QMessageBox.question(
            self, '提示', '确定要退出吗?',
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            sys.stdout = sys.__stdout__
            event.accept()
            res = super().closeEvent(event)
        else:
            event.ignore()
            res = None
        return res

    def onUpdateText(self, text):
        """
        重定向控制台输出到文本框控件
        """
        if text == '\n':
            # print函数默认会在输出内容后自动添加一个换行符\n
            # 后续脚本能够保证,每次显示print内容时,都是从一个新行的行首开始,不再需要这个换行符,跳过即可
            return
        cursor = self.textEdit.textCursor()

        if cursor.position() == cursor.block().position():
            # 如果当前光标位置是行首, 直接打印文字内容
            cursor.insertText(text)
        else:
            # 如果当前光标位置是行中, 换行,从下一行行首开始打印文字内容
            cursor.movePosition(cursor.MoveOperation.End)
            cursor.insertText('\n' + text)
        self.textEdit.setTextCursor(cursor)
        self.textEdit.ensureCursorVisible()
        return

    def error_func(self):
        """
        执行后会报错的程序
        """
        self.logger.info('运行存在异常的程序脚本')
        a = [1, 2, 3, 4]
        self.logger.info(f'程序运行结果: {a[5]}')

    def normal_func(self):
        """
        正常运行的程序
        """
        self.logger.info('INFO级别:用于展示程序的运行状态和正常操作')
        self.logger.debug('DEBUG级别:用于输出详细的运行日志')
        self.logger.warning(
            'WARNING级别:用于记录一些警告信息,这些信息可能表明程序出现了一些异常或潜在的问题,但并不会导致程序的崩溃或功能的异常')
        self.logger.error('ERROR级别:用于记录程序中的错误信息,但仍不影响系统的继续运行')
        self.logger.critical('CRITICAL级别:最高级别的日志,用于记录知名错误,这些错误会导致程序立即退出,并且无法恢复;')
        self.logger.fatal('FATAL级别:与CRITICAL一样表示最高级别的日志,在不同的框架或库中,可能会定义不同的日志级别,'
                          'Python标准日志模块中没有定义FATAL级别,某些第三方日志库可能会使用FATAL级别表示同样的错误')
        a = [1, 2, 3, 4]
        self.logger.info(f'程序运行结果: {a[2]}')
        print('这是第一段print文字,默认从新的一行行首开始显示')
        print('这是第二段print文字,默认从新的一行行首开始显示')
        print('这是第三段print文字')

    def redirection_msg(self, mode, context, message):
        """
        打印错误信息并且弹出警告窗口
        """
        frame = inspect.currentframe().f_back
        filename = frame.f_code.co_filename
        lineno = frame.f_lineno
        funcname = frame.f_code.co_name
        if mode == QtMsgType.QtInfoMsg:
            mode = 20
        elif mode == QtMsgType.QtWarningMsg:
            mode = 30
        elif mode == QtMsgType.QtCriticalMsg:
            mode = 50
        elif mode == QtMsgType.QtFatalMsg:
            mode = 50
        else:
            mode = 10

        record = self.logger.makeRecord(name=self.logger.name, level=mode, fn=filename, lno=lineno, msg=message, args=(), exc_info=None, func=funcname, sinfo=None)
        self.logger.handle(record)


def error_handler(exc_type, exc_value, exc_tb):
    error_message = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
    reply = QMessageBox.critical(
        None, 'Error Caught!:', error_message,
        QMessageBox.StandardButton.Abort | QMessageBox.StandardButton.Retry,
        QMessageBox.StandardButton.Abort)
    if reply == QMessageBox.StandardButton.Abort:
        sys.exit(1)


if __name__ == '__main__':
    sys.excepthook = error_handler
    app = QApplication(sys.argv)
    ui = Ui_Form()
    ui.show()
    sys.exit(app.exec())

4. 运行效果展示

QTextEdit控件中显示日志信息、print信息,以及运行异常时的报错信息;此外,当程序报错时,会出现弹窗显示具体的报错内容,报错内容将保存到log文件,弹窗关闭后,程序将强制退出。

即便出现异常,程序也不会闪退

报错信息本地文件可见。

附注

还可以把print的部分直接作为info级别的log输出,只需要小小的改动一下onUpdateText函数。

    def onUpdateText(self, text):
        """
        重定向控制台输出到文本框控件
        """
        if text == '\n':
            # print函数默认会在输出内容后自动添加一个换行符\n
            # 后续脚本能够保证,每次显示print内容时,都是从一个新行的行首开始,不再需要这个换行符,跳过即可
            return
        self.logger.info(text)
        # cursor = self.textEdit.textCursor()
        #
        # if cursor.position() == cursor.block().position():
        #     # 如果当前光标位置是行首, 直接打印文字内容
        #     cursor.insertText(text)
        # else:
        #     # 如果当前光标位置是行中, 换行,从下一行行首开始打印文字内容
        #     cursor.movePosition(cursor.MoveOperation.End)
        #     cursor.insertText('\n'+text)
        # self.textEdit.setTextCursor(cursor)
        # self.textEdit.ensureCursorVisible()
        return

效果如下:

参考链接

1. python logging模块详解_logging.getlogger(__name__)-CSDN博客

2. PyQt5 重定向输出以及错误信息_python pyqt5重定向输出流-CSDN博客

  • 19
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值