更新版本

This commit is contained in:
2026-06-12 16:01:53 +08:00
parent 62ebc2f589
commit d17a54eb73
23 changed files with 8448 additions and 52 deletions
+209
View File
@@ -0,0 +1,209 @@
"""
日志模块 - 提供统一的日志记录功能
功能说明:
- 日志写入文件 (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, QTextEdit
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):
"""
日志显示框架组件
继承自 QFrameQt 框架组件)和 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)
# 创建日志格式化器:时间 [级别] 消息
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: 格式化后的日志消息字符串
实现细节:
- 使用 findChild 查找 TextEdit 组件
- 使用 append 方法追加日志消息,自动换行
"""
self.textEdit.append(message)
# text_edit = self.findChild(QTextEdit, "textEdit")
# if text_edit:
# text_edit.append(message)
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FrameLog</class>
<widget class="QFrame" name="FrameLog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>982</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Frame</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTextEdit" name="textEdit"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+42
View File
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'log.ui'
##
## Created by: Qt User Interface Compiler version 6.8.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QSizePolicy,
QTextEdit, QWidget)
class Ui_FrameLog(object):
def setupUi(self, FrameLog):
if not FrameLog.objectName():
FrameLog.setObjectName(u"FrameLog")
FrameLog.resize(982, 300)
self.gridLayout = QGridLayout(FrameLog)
self.gridLayout.setObjectName(u"gridLayout")
self.textEdit = QTextEdit(FrameLog)
self.textEdit.setObjectName(u"textEdit")
self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1)
self.retranslateUi(FrameLog)
QMetaObject.connectSlotsByName(FrameLog)
# setupUi
def retranslateUi(self, FrameLog):
FrameLog.setWindowTitle(QCoreApplication.translate("FrameLog", u"Frame", None))
# retranslateUi