228 lines
8.8 KiB
Python
228 lines
8.8 KiB
Python
#!/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)
|