Files
WQ_GUI/src/gui/core/log_manager.py
2026-07-01 17:33:52 +08:00

228 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
日志与进度管理器
将主窗口中的日志区(QTextEdit)、进度条(QProgressBar)和"清空日志"按钮
的 UI 创建与控制逻辑完全封装。
职责:
- create_log_panel() → 返回组装好的 QWidget(日志 + 进度条)
- 内部订阅 LogMessage / ProgressUpdate 事件,自动更新 UI
- 主窗口无需再关心日志/进度的状态同步
订阅的事件:
LogMessage → {message, level} 写入日志区
ProgressUpdate → {percentage, message} 更新进度条
"""
import html
from datetime import datetime
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QGroupBox, QTextEdit, QProgressBar, QPushButton,
)
from src.gui.core.event_bus import global_event_bus
_MAX_LOG_LINES = 2000 # 日志行数上限,防止长时间运行内存耗尽
class LogManager(QObject):
"""日志与进度管理器。
用法::
log_mgr = LogManager(parent=self)
log_panel = log_mgr.create_log_panel()
layout.addWidget(log_panel)
# 之后所有日志/进度更新由 EventBus 自动驱动,无需手动操作
"""
def __init__(self, parent=None):
super().__init__(parent)
self._log_text: QTextEdit = None
self._progress_bar: QProgressBar = None
# 订阅事件
global_event_bus.subscribe('LogMessage', self._on_log_message)
global_event_bus.subscribe('ProgressUpdate', self._on_progress_update)
# ★ 父控件销毁时自动取消订阅,防止回调泄漏
if parent is not None:
parent.destroyed.connect(self._cleanup)
def _cleanup(self):
"""清理 EventBus 订阅。"""
global_event_bus.unsubscribe('LogMessage', self._on_log_message)
global_event_bus.unsubscribe('ProgressUpdate', self._on_progress_update)
# ═══════════════════════════════════════════════════════════
# 公开 API
# ═══════════════════════════════════════════════════════════
def create_log_panel(self) -> QWidget:
"""创建并返回日志+进度面板的组装 Widget。
Returns:
QWidget: 包含日志区(QGroupBox)和进度条(QGroupBox)的垂直布局容器
"""
from src.gui.styles import ModernStylesheet
container = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
# ── 日志区 ──
log_group = QGroupBox("执行日志")
log_group.setStyleSheet(f"""
QGroupBox {{
background-color: {ModernStylesheet.COLORS['panel_bg']};
border: 1px solid {ModernStylesheet.COLORS['border_light']};
border-radius: 5px; margin-top: 8px; padding-top: 15px;
padding-left: 9px; padding-right: 9px; padding-bottom: 9px;
}}
QGroupBox::title {{
subcontrol-origin: margin; subcontrol-position: top left;
padding: 0 5px; font-weight: bold;
color: {ModernStylesheet.COLORS['text_primary']};
}}
""")
log_layout = QVBoxLayout()
log_layout.setContentsMargins(5, 5, 5, 5)
self._log_text = QTextEdit()
self._log_text.setReadOnly(True)
self._log_text.setMaximumHeight(200)
self._log_text.setStyleSheet(f"""
QTextEdit {{
background-color: {ModernStylesheet.COLORS['panel_bg']};
color: {ModernStylesheet.COLORS['text_primary']};
border: 1px solid {ModernStylesheet.COLORS['border']};
border-radius: 4px; padding: 5px;
font-family: 'Courier New', monospace; font-size: 10px;
}}
""")
log_layout.addWidget(self._log_text)
clear_btn = QPushButton("清空日志")
clear_btn.setMaximumWidth(100)
clear_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal'))
clear_btn.clicked.connect(self.clear_log)
btn_row = QHBoxLayout()
btn_row.addWidget(clear_btn)
btn_row.addStretch()
log_layout.addLayout(btn_row)
log_group.setLayout(log_layout)
layout.addWidget(log_group, 1)
# ── 进度条 ──
progress_group = QGroupBox("执行进度")
progress_group.setStyleSheet(f"""
QGroupBox {{
background-color: {ModernStylesheet.COLORS['panel_bg']};
border: 1px solid {ModernStylesheet.COLORS['border_light']};
border-radius: 5px; margin-top: 8px; padding-top: 10px;
padding-left: 9px; padding-right: 9px; padding-bottom: 9px;
}}
QGroupBox::title {{
subcontrol-origin: margin; subcontrol-position: top left;
padding: 0 5px; font-weight: bold;
color: {ModernStylesheet.COLORS['text_primary']};
}}
""")
progress_layout = QVBoxLayout()
progress_layout.setContentsMargins(5, 5, 5, 5)
self._progress_bar = QProgressBar()
self._progress_bar.setValue(0)
self._progress_bar.setStyleSheet(f"""
QProgressBar {{
background-color: {ModernStylesheet.COLORS['panel_bg']};
border: 1px solid {ModernStylesheet.COLORS['border']};
border-radius: 4px; padding: 2px; text-align: center; height: 20px;
}}
QProgressBar::chunk {{
background-color: {ModernStylesheet.COLORS['success']}; border-radius: 3px;
}}
""")
progress_layout.addWidget(self._progress_bar)
progress_group.setLayout(progress_layout)
layout.addWidget(progress_group, 0)
container.setLayout(layout)
return container
def clear_log(self):
"""清空日志区。"""
if self._log_text is not None:
self._log_text.clear()
def info(self, message: str):
"""便捷方法:发布 info 级别日志。"""
global_event_bus.publish('LogMessage', {
'message': message, 'level': 'info',
})
def warning(self, message: str):
"""便捷方法:发布 warning 级别日志。"""
global_event_bus.publish('LogMessage', {
'message': message, 'level': 'warning',
})
def error(self, message: str):
"""便捷方法:发布 error 级别日志。"""
global_event_bus.publish('LogMessage', {
'message': message, 'level': 'error',
})
@property
def progress_bar(self) -> QProgressBar:
return self._progress_bar
@property
def log_text(self) -> QTextEdit:
return self._log_text
# ═══════════════════════════════════════════════════════════
# EventBus 订阅回调
# ═══════════════════════════════════════════════════════════
def _on_log_message(self, data: dict):
"""LogMessage 事件回调:写入日志区。"""
if self._log_text is None:
return
if not isinstance(data, dict):
return
message = data.get('message', '')
level = data.get('level', 'info')
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# ★ HTML 转义:防止日志消息中的 < > & 破坏 HTML 渲染
safe_message = html.escape(str(message))
color_map = {'error': 'red', 'warning': 'orange'}
color = color_map.get(level, 'black')
formatted = f'<span style="color: {color};">[{timestamp}] {safe_message}</span>'
self._log_text.append(formatted)
# ★ 日志行数上限:防止长时间运行内存耗尽
if self._log_text.document().blockCount() > _MAX_LOG_LINES + 100:
self._log_text.clear()
self._log_text.append(
f'<span style="color: gray;">[日志已自动清空,达到 {_MAX_LOG_LINES} 行上限]</span>'
)
cursor = self._log_text.textCursor()
cursor.movePosition(QTextCursor.End)
self._log_text.setTextCursor(cursor)
def _on_progress_update(self, data: dict):
"""ProgressUpdate 事件回调:更新进度条。"""
if self._progress_bar is None:
return
percentage = data.get('percentage', 0)
self._progress_bar.setValue(percentage)