import threading from datetime import datetime import tkinter as tk import ttkbootstrap as ttk 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 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"): if threading.current_thread() is not threading.main_thread(): self.after(0, self.log, message, level) return text = "" if message is None else str(message) normalized_level = self._normalize_level(level, text) 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} 条日志")