""" 日志模块 - 提供统一的日志记录功能 功能说明: - 日志写入文件 (logs/log_file.log) - 日志输出到控制台 - 日志显示到 UI 的 TextEdit 组件 架构设计: - 使用组合模式避免 PySide6 QObject.emit() 方法冲突 - LogHandler: 独立的日志处理器,继承自 logging.Handler - FrameLog: UI 组件,负责日志配置和显示 日志格式:%(asctime)s [%(levelname)s] %(message)s 时间格式:%Y-%m-%d %H:%M:%S """ import logging from pathlib import Path from typing import Callable, Optional from PySide6.QtCore import QObject, Signal from PySide6.QtWidgets import QFrame from .log_ui import Ui_FrameLog # 日志配置常量 LOG_FILE = "logs/log_file.log" # 日志文件路径 CONSOLE_OUTPUT = True # 是否输出到控制台 class LogHandler(logging.Handler): """ 独立的日志处理器 继承自 logging.Handler,用于处理日志记录的格式化输出 使用回调函数机制避免与 PySide6 QObject.emit() 方法名冲突 设计原因: - logging.Handler.emit() 和 QObject.emit() 方法签名冲突 - 使用组合模式,将回调函数作为日志传递的桥梁 """ def __init__(self, callback: Callable[[str], None]) -> None: """ 初始化日志处理器 Args: callback: 回调函数,接收格式化后的日志消息字符串 """ super().__init__() self.callback = callback def emit(self, record: logging.LogRecord) -> None: """ 处理日志记录 当日志系统调用此方法时,将格式化后的日志消息传递给回调函数 Args: record: 日志记录对象,包含级别、消息等信息 异常处理: - RecursionError: 防止递归调用导致无限循环 - 其他异常: 使用 handleError 记录错误 """ try: msg = self.format(record) self.callback(msg) except RecursionError: self.handleError(record) except Exception: self.handleError(record) class FrameLog(QFrame, Ui_FrameLog): """ 日志显示框架组件 继承自 QFrame(Qt 框架组件)和 Ui_FrameLog(自动生成的 UI 界面) 负责日志系统的配置和 UI 显示功能 功能: - 初始化日志系统(文件、控制台、UI) - 将日志消息显示到 TextEdit 组件 - 通过信号机制实现线程安全的 UI 更新 """ # PySide6 信号定义,用于跨线程传递日志消息 log_signal = Signal(str) def __init__(self, parent: Optional[QObject] = None) -> None: """ 初始化日志框架 Args: parent: 父组件对象,默认为 None """ QFrame.__init__(self, parent) self.setupUi(self) self._init_logging() self._connect_signal() def _init_logging(self) -> None: """ 初始化日志系统 配置日志系统的各个组件: - 设置日志级别为 DEBUG - 配置日志格式化器 - 设置文件日志处理器(可选) - 设置控制台日志处理器(可选) - 配置 UI 日志处理器 """ logger = logging.getLogger() logger.setLevel(logging.DEBUG) # 抑制第三方库 DEBUG 日志刷屏 logging.getLogger("PySide6").setLevel(logging.WARNING) logging.getLogger("qt_material").setLevel(logging.WARNING) # 创建日志格式化器:时间 [级别] 消息 formatter = logging.Formatter( "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) # 配置文件日志处理器(如果启用) if LOG_FILE: self._setup_file_handler(logger, formatter) # 配置控制台日志处理器(如果启用) if CONSOLE_OUTPUT: self._setup_console_handler(logger, formatter) # 配置 UI 日志处理器 self._log_handler = LogHandler(self._update_log_display) self._log_handler.setFormatter(formatter) self._log_handler.setLevel(logging.DEBUG) logger.addHandler(self._log_handler) def _setup_file_handler(self, logger: logging.Logger, formatter: logging.Formatter) -> None: """ 配置日志文件处理器 将日志写入指定文件,支持 UTF-8 编码 Args: logger: 日志记录器实例 formatter: 日志格式化器 异常处理: - OSError: 操作系统错误(如磁盘满) - PermissionError: 权限不足 - 失败时降级到控制台输出 """ try: log_path = Path(LOG_FILE) log_path.parent.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(log_path, encoding='utf-8') file_handler.setFormatter(formatter) file_handler.setLevel(logging.DEBUG) logger.addHandler(file_handler) except (OSError, PermissionError) as e: logging.warning(f"无法创建日志文件 {LOG_FILE}: {e}") logging.warning("日志将仅输出到控制台") def _setup_console_handler(self, logger: logging.Logger, formatter: logging.Formatter) -> None: """ 配置日志控制台处理器 将日志输出到标准控制台(stdout) Args: logger: 日志记录器实例 formatter: 日志格式化器 注意: - 控制台输出级别设置为 INFO,只显示重要信息 - DEBUG 级别的日志不会输出到控制台 """ console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) console_handler.setLevel(logging.INFO) logger.addHandler(console_handler) def _connect_signal(self) -> None: """ 连接日志信号到 UI 更新方法 将 log_signal 信号连接到 _update_log_display 方法 实现日志消息的线程安全传递 """ self.log_signal.connect(self._update_log_display) def _update_log_display(self, message: str) -> None: """更新 UI 的日志显示组件。 将日志消息追加到 TextEdit 组件中显示。 Args: message: 格式化后的日志消息字符串 """ self.textEdit.append(message)