"""日志模块:UI 文本控件 + 控制台/文件输出,线程安全。 UI 更新不直接在任意线程操作 Tk 控件,而是写入 queue; 主线程通过 drain() 周期性取走并写入文本框(见 app._refresh_ui)。 """ import os import queue import threading import time class AppLogger: def __init__(self, widget=None, file_path=None, max_lines=200, console=True): self.widget = widget self.file_path = file_path self.max_lines = max_lines self.console = console self._lock = threading.Lock() self._queue = queue.Queue() if file_path: os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True) def _format(self, level, msg): return f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [{level}] {msg}" def info(self, msg): self._write("INFO", msg) def warning(self, msg): self._write("WARN", msg) def error(self, msg): self._write("ERROR", msg) def _write(self, level, msg): line = self._format(level, msg) self._queue.put(line) with self._lock: if self.console: print(line) if self.file_path: try: with open(self.file_path, "a", encoding="utf-8") as f: f.write(line + "\n") except OSError: pass def drain(self): """主线程调用:把队列中的日志写入 UI 文本框。""" if self.widget is None or self._queue.empty(): return try: self.widget.config(state="normal") while not self._queue.empty(): self.widget.insert("end", self._queue.get_nowait() + "\n") count = int(self.widget.index("end-1c").split(".")[0]) if count > self.max_lines: self.widget.delete("1.0", f"{count - self.max_lines}.0") self.widget.see("end") self.widget.config(state="disabled") except Exception: # 窗口销毁等异常静默 pass