123 lines
4.2 KiB
Python
123 lines
4.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
import logging
|
|
import queue
|
|
import tkinter as tk
|
|
from logging.handlers import QueueHandler, QueueListener
|
|
from tkinter.scrolledtext import ScrolledText
|
|
|
|
class TkinterLogHandler(logging.Handler):
|
|
"""
|
|
Redirects logs to a Tkinter text widget in a thread-safe manner.
|
|
"""
|
|
def __init__(self, text_widget: ScrolledText):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
|
|
def emit(self, record: logging.LogRecord):
|
|
"""
|
|
Thread-safe emit method using Tkinter's 'after' to schedule
|
|
updates on the main loop.
|
|
"""
|
|
try:
|
|
message = self.format(record)
|
|
# Use level first letter as tag (D, I, W, E) for coloring
|
|
tag = record.levelname[0]
|
|
|
|
# Schedule the actual UI update to run on the main thread
|
|
self.text_widget.after(0, self._safe_append, message, tag)
|
|
except Exception:
|
|
self.handleError(record)
|
|
|
|
def _safe_append(self, message: str, tag: str):
|
|
"""
|
|
Executes the widget modification. This method is always
|
|
called from the main thread.
|
|
"""
|
|
self.text_widget.configure(state='normal')
|
|
self.text_widget.insert(tk.END, message + '\n', tag)
|
|
self.text_widget.configure(state='disabled')
|
|
self.text_widget.yview(tk.END)
|
|
|
|
class CustomLogger:
|
|
"""
|
|
Singleton logger manager for PyMsc.
|
|
Handles multi-threaded logging to console, file, and Tkinter UI.
|
|
"""
|
|
_instance = None
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
if not cls._instance:
|
|
cls._instance = super(CustomLogger, cls).__new__(cls)
|
|
return cls._instance
|
|
|
|
def __init__(self, log_file: str = 'pymsc.log', use_console: bool = True):
|
|
if hasattr(self, 'initialized'):
|
|
return
|
|
|
|
self.initialized = True
|
|
self.log_queue = queue.Queue()
|
|
|
|
# Main logger configuration
|
|
self.logger = logging.getLogger('PyMsc')
|
|
self.logger.setLevel(logging.DEBUG)
|
|
|
|
# Standard formatter
|
|
formatter = logging.Formatter(
|
|
'%(asctime)s - %(levelname)s - %(message)s',
|
|
datefmt='%y%m%d%H%M%S'
|
|
)
|
|
|
|
# 1. Console Handler
|
|
if use_console:
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setFormatter(formatter)
|
|
self.logger.addHandler(console_handler)
|
|
|
|
# 2. File Handler
|
|
file_handler = logging.FileHandler(log_file)
|
|
file_handler.setFormatter(formatter)
|
|
self.logger.addHandler(file_handler)
|
|
|
|
self.listener = None
|
|
|
|
def setup_tkinter_logging(self, parent_frame: tk.Frame):
|
|
"""
|
|
Initializes the ScrolledText widget and starts the QueueListener
|
|
to capture logs from all threads.
|
|
"""
|
|
# UI Component for logs
|
|
self.scrolled_text = ScrolledText(
|
|
parent_frame,
|
|
state='disabled',
|
|
height=5,
|
|
bg="black",
|
|
fg="white",
|
|
font=("Consolas", 8)
|
|
)
|
|
self.scrolled_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
|
|
|
|
# Define color tags matching record.levelname[0]
|
|
self.scrolled_text.tag_configure('D', foreground='lightgray') # Debug
|
|
self.scrolled_text.tag_configure('I', foreground='white') # Info
|
|
self.scrolled_text.tag_configure('W', foreground='yellow') # Warning
|
|
self.scrolled_text.tag_configure('E', foreground='red') # Error
|
|
|
|
# Attach QueueHandler to the logger to redirect all logs to the queue
|
|
queue_handler = QueueHandler(self.log_queue)
|
|
self.logger.addHandler(queue_handler)
|
|
|
|
# Create the custom Tkinter handler
|
|
tk_handler = TkinterLogHandler(self.scrolled_text)
|
|
|
|
# QueueListener will pull from queue and call tk_handler.emit in its thread
|
|
self.listener = QueueListener(self.log_queue, tk_handler)
|
|
self.listener.start()
|
|
|
|
def get_logger(self) -> logging.Logger:
|
|
"""Returns the logger instance."""
|
|
return self.logger
|
|
|
|
def stop_listener(self):
|
|
"""Stops the queue listener thread safely."""
|
|
if self.listener:
|
|
self.listener.stop() |