# -*- 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()