Files
pqAutomationApp/app/views/pq_log_gui.py
2026-04-29 16:43:31 +08:00

188 lines
6.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.
import logging
import threading
from datetime import datetime
import tkinter as tk
import ttkbootstrap as ttk
# 与 app.logging_setup 共享的映射;放在这里避免循环 import
_GUI_LEVEL_TO_LOG = {
"debug": logging.DEBUG,
"info": logging.INFO,
"success": logging.INFO,
"warning": logging.WARNING,
"error": logging.ERROR,
"separator": logging.INFO,
"blank": logging.DEBUG,
}
class PQLogGUI(ttk.Frame):
VALID_LEVELS = {"info", "success", "warning", "error", "debug", "separator", "blank"}
def __init__(self, parent):
super().__init__(parent)
self._line_count = 0
self._max_lines = 1500
# 与 stdlib logging 联通的 logger由 logging_setup 配置 file handler
self._logger = logging.getLogger("pq.gui")
self.create_widgets()
def create_widgets(self):
log_frame = ttk.LabelFrame(self, text="测试日志")
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
toolbar = ttk.Frame(log_frame)
toolbar.pack(fill=tk.X, padx=6, pady=(6, 2))
self.log_summary_var = tk.StringVar(value="0 条日志")
ttk.Label(
toolbar,
textvariable=self.log_summary_var,
bootstyle="secondary",
).pack(side=tk.LEFT)
ttk.Button(
toolbar,
text="清空日志",
command=self.clear_log,
bootstyle="secondary-outline",
width=10,
).pack(side=tk.RIGHT)
text_container = ttk.Frame(log_frame)
text_container.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))
self.log_text = tk.Text(
text_container,
height=10,
width=50,
wrap=tk.WORD,
font=("Consolas", 10),
bg="#fbfcfe",
fg="#1f2937",
relief=tk.FLAT,
bd=0,
padx=10,
pady=8,
spacing1=2,
spacing3=2,
insertbackground="#1f2937",
)
self.log_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
log_scrollbar = ttk.Scrollbar(text_container, command=self.log_text.yview)
log_scrollbar.pack(fill=tk.Y, side=tk.RIGHT)
self.log_text.config(yscrollcommand=log_scrollbar.set)
self._configure_tags()
self.log_text.config(state=tk.DISABLED)
def log(self, message, level="info", _from_logging=False):
if threading.current_thread() is not threading.main_thread():
self.after(0, self.log, message, level, _from_logging)
return
text = "" if message is None else str(message)
normalized_level = self._normalize_level(level, text)
# 转发到 stdlib logging落到文件_from_logging=True 表示来源已是
# logging不再回写避免重复。带 _from_gui 标记TkLogHandler 会跳过。
if not _from_logging and normalized_level != "blank":
log_level = _GUI_LEVEL_TO_LOG.get(normalized_level, logging.INFO)
try:
self._logger.log(
log_level, text, extra={"_from_gui": True}
)
except Exception:
pass
self.log_text.config(state=tk.NORMAL)
self._append_message(text, normalized_level)
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def clear_log(self):
if threading.current_thread() is not threading.main_thread():
self.after(0, self.clear_log)
return
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)
self._line_count = 0
self._update_summary()
def _configure_tags(self):
self.log_text.tag_configure("timestamp", foreground="#6b7280")
self.log_text.tag_configure("level_info", foreground="#2563eb")
self.log_text.tag_configure("level_success", foreground="#0f766e")
self.log_text.tag_configure("level_warning", foreground="#b45309")
self.log_text.tag_configure("level_error", foreground="#b91c1c")
self.log_text.tag_configure("level_debug", foreground="#7c3aed")
self.log_text.tag_configure("message", foreground="#1f2937")
self.log_text.tag_configure("message_success", foreground="#0f766e")
self.log_text.tag_configure("message_warning", foreground="#b45309")
self.log_text.tag_configure("message_error", foreground="#991b1b")
self.log_text.tag_configure("message_debug", foreground="#6d28d9")
self.log_text.tag_configure("separator", foreground="#94a3b8")
self.log_text.tag_configure("traceback", foreground="#7f1d1d")
self.log_text.tag_configure("blank", spacing1=4, spacing3=4)
def _append_message(self, message, level):
lines = message.splitlines() or [""]
for line in lines:
self._append_line(line, level)
self._trim_excess_lines()
self._update_summary()
def _append_line(self, line, level):
timestamp = datetime.now().strftime("%H:%M:%S")
rendered = "" if line is None else str(line).strip()
if level == "blank" or not rendered:
self.log_text.insert(tk.END, "\n", ("blank",))
self._line_count += 1
return
if level == "separator":
self.log_text.insert(tk.END, f"[{timestamp}] ", ("timestamp",))
self.log_text.insert(tk.END, "[SECTION] ", ("level_info",))
self.log_text.insert(tk.END, rendered + "\n", ("separator",))
self._line_count += 1
return
level_tag = f"level_{level}"
level_label = level.upper().ljust(7)
if level == "error" and rendered.startswith("Traceback"):
message_tag = "traceback"
elif level in {"success", "warning", "error", "debug"}:
message_tag = f"message_{level}"
else:
message_tag = "message"
self.log_text.insert(tk.END, f"[{timestamp}] ", ("timestamp",))
self.log_text.insert(tk.END, f"[{level_label}] ", (level_tag,))
self.log_text.insert(tk.END, rendered + "\n", (message_tag,))
self._line_count += 1
def _normalize_level(self, level, message):
normalized = "info" if level is None else str(level).strip().lower()
if normalized not in self.VALID_LEVELS:
normalized = "info"
if normalized == "info" and (message is None or str(message).strip() == ""):
return "blank"
return normalized
def _trim_excess_lines(self):
overflow = self._line_count - self._max_lines
if overflow <= 0:
return
self.log_text.delete("1.0", f"{overflow + 1}.0")
self._line_count = self._max_lines
def _update_summary(self):
self.log_summary_var.set(f"{self._line_count} 条日志")