Python Logging to Tkinter Text Widget

后端 未结 6 1847
悲哀的现实
悲哀的现实 2020-12-14 16:46

Does any one out there have an example of how to setup logging in Python to a Tkinter Text Widget? I have seen this used in several apps but cannot figure out how to direct

相关标签:
6条回答
  • 2020-12-14 17:26

    In addition to the above answers: even though there are a lot of proposed solutions for this (here and also in this other thread), I was struggling quite a bit to make this work myself. Eventually I ran into this text handler class by Moshe Kaplan, which uses a ScrolledText widget (which is probably easier than the ScrollBar method).

    It took me some time to figure out how to actually use Moshe's class in a threaded application. In the end I created a minimal demo script that shows how to make it all work. As it might be helpful to others I'm sharing it below. In my particular case I wanted to log to both the GUI and to a text file; if you don't need that just remove the filename attribute in logging.basicConfig.

    import time
    import threading
    import logging
    try:
        import tkinter as tk # Python 3.x
        import tkinter.scrolledtext as ScrolledText
    except ImportError:
        import Tkinter as tk # Python 2.x
        import ScrolledText
    
    class TextHandler(logging.Handler):
        # This class allows you to log to a Tkinter Text or ScrolledText widget
        # Adapted from Moshe Kaplan: https://gist.github.com/moshekaplan/c425f861de7bbf28ef06
    
        def __init__(self, text):
            # run the regular Handler __init__
            logging.Handler.__init__(self)
            # Store a reference to the Text it will log to
            self.text = text
    
        def emit(self, record):
            msg = self.format(record)
            def append():
                self.text.configure(state='normal')
                self.text.insert(tk.END, msg + '\n')
                self.text.configure(state='disabled')
                # Autoscroll to the bottom
                self.text.yview(tk.END)
            # This is necessary because we can't modify the Text from other threads
            self.text.after(0, append)
    
    class myGUI(tk.Frame):
    
        # This class defines the graphical user interface 
    
        def __init__(self, parent, *args, **kwargs):
            tk.Frame.__init__(self, parent, *args, **kwargs)
            self.root = parent
            self.build_gui()
    
        def build_gui(self):                    
            # Build GUI
            self.root.title('TEST')
            self.root.option_add('*tearOff', 'FALSE')
            self.grid(column=0, row=0, sticky='ew')
            self.grid_columnconfigure(0, weight=1, uniform='a')
            self.grid_columnconfigure(1, weight=1, uniform='a')
            self.grid_columnconfigure(2, weight=1, uniform='a')
            self.grid_columnconfigure(3, weight=1, uniform='a')
    
            # Add text widget to display logging info
            st = ScrolledText.ScrolledText(self, state='disabled')
            st.configure(font='TkFixedFont')
            st.grid(column=0, row=1, sticky='w', columnspan=4)
    
            # Create textLogger
            text_handler = TextHandler(st)
    
            # Logging configuration
            logging.basicConfig(filename='test.log',
                level=logging.INFO, 
                format='%(asctime)s - %(levelname)s - %(message)s')        
    
            # Add the handler to logger
            logger = logging.getLogger()        
            logger.addHandler(text_handler)
    
    def worker():
        # Skeleton worker function, runs in separate thread (see below)   
        while True:
            # Report time / date at 2-second intervals
            time.sleep(2)
            timeStr = time.asctime()
            msg = 'Current time: ' + timeStr
            logging.info(msg) 
    
    def main():
    
        root = tk.Tk()
        myGUI(root)
    
        t1 = threading.Thread(target=worker, args=[])
        t1.start()
    
        root.mainloop()
        t1.join()
    
    main()
    

    Github Gist link to above code here:

    https://gist.github.com/bitsgalore/901d0abe4b874b483df3ddc4168754aa

    0 讨论(0)
  • 2020-12-14 17:27

    I've bumped the same problem. The common solution found was like mentioned in this thread- bringing widget from GUI module to Logging.Handler.
    It was criticized as not safety, as a widget is passes to another thread (logging).
    The best solution found was using queue, made by Benjamin Bertrand: https://beenje.github.io/blog/posts/logging-to-a-tkinter-scrolledtext-widget/.
    I improved it a bit: queue should be created in logging handler, and passed to a GUI widget.

    import logger
    import queue
    
    
    class QueueHandler(logging.Handler):
        def __init__(self):
            super().__init__()
            self.log_queue = queue.Queue()
    
        def emit(self, record):
            # put a formatted message to queue
            self.log_queue.put(self.format(record))
    
    
    queue_handler = QueueHandler()
    logger.addHandler(queue_handler)
    
    

    So we have generally available logger, and every log message from any module will be passed to a queue_handler along with other handlers - to a file etc. Now we can import queue_handler to a widget.
    Most of the code is taken from the link above, I just changed the location of the original logging queue.

    from tkinter.scrolledtext import ScrolledText
    from  some_logging_module import queue_handler
    
    class ConsoleUi:
        """Poll messages from a logging queue and display them in a scrolled text widget"""
    
        def __init__(self, frame, queue):
            self.frame = frame
            # Create a ScrolledText wdiget
            self.console = ScrolledText(frame)
            self.console.configure(font='TkFixedFont')
            self.console.pack(padx=10, pady=10, fill=BOTH, expand=True)
            self.log_queue = queue
            # Start polling messages from the queue
            self.frame.after(100, self.poll_log_queue)
    
        def display(self, msg):
            self.console.configure(state='normal')
            self.console.insert(END, msg + '\n')
            self.console.configure(state='disabled')
            # Autoscroll to the bottom
            self.console.yview(END)
    
        def poll_log_queue(self):
            # Check every 100ms if there is a new message in the queue to display
            while not self.log_queue.empty():
                msg = self.log_queue.get(block=False)
                self.display(msg)
            self.frame.after(100, self.poll_log_queue)
    
    

    As result a widget displays all logging data of an app.

    0 讨论(0)
  • 2020-12-14 17:32

    Building further on ford's answer, here's a scrolling Text widget that tails the log. The logging_handler member is what you add to your logger.

    import logging
    from Tkinter import END, N, S, E, W, Scrollbar, Text
    import ttk
    
    class LoggingHandlerFrame(ttk.Frame):
    
        class Handler(logging.Handler):
            def __init__(self, widget):
                logging.Handler.__init__(self)
                self.setFormatter(logging.Formatter("%(asctime)s: %(message)s"))
                self.widget = widget
                self.widget.config(state='disabled')
    
            def emit(self, record):
                self.widget.config(state='normal')
                self.widget.insert(END, self.format(record) + "\n")
                self.widget.see(END)
                self.widget.config(state='disabled')
    
        def __init__(self, *args, **kwargs):
            ttk.Frame.__init__(self, *args, **kwargs)
    
            self.columnconfigure(0, weight=1)
            self.columnconfigure(1, weight=0)
            self.rowconfigure(0, weight=1)
    
            self.scrollbar = Scrollbar(self)
            self.scrollbar.grid(row=0, column=1, sticky=(N,S,E))
    
            self.text = Text(self, yscrollcommand=self.scrollbar.set)
            self.text.grid(row=0, column=0, sticky=(N,S,E,W))
    
            self.scrollbar.config(command=self.text.yview)
    
            self.logging_handler = LoggingHandlerFrame.Handler(self.text)
    
    0 讨论(0)
  • 2020-12-14 17:37

    I built on Yuri's idea, but needed to make a few changes to get things working:

    import logging
    import Tkinter as tk
    
    class WidgetLogger(logging.Handler):
        def __init__(self, widget):
            logging.Handler.__init__(self)
            self.setLevel(logging.INFO)
            self.widget = widget
            self.widget.config(state='disabled')
    
        def emit(self, record):
            self.widget.config(state='normal')
            # Append message (record) to the widget
            self.widget.insert(tk.END, self.format(record) + '\n')
            self.widget.see(tk.END)  # Scroll to the bottom
            self.widget.config(state='disabled')
    

    Note that toggling the state back and forth from 'normal' to 'disabled' is necessary to make the Text widget read-only.

    0 讨论(0)
  • 2020-12-14 17:38

    You should subclass logging.Handler, e.g.:

    import logging
    from Tkinter import INSERT
    
    class WidgetLogger(logging.Handler):
        def __init__(self, widget):
            logging.Handler.__init__(self)
            self.widget = widget
    
        def emit(self, record):
            # Append message (record) to the widget
            self.widget.insert(INSERT, record + '\n')
    
    0 讨论(0)
  • 2020-12-14 17:39

    Building on ford's too but adding colored text !

    class WidgetLogger(logging.Handler):
        def __init__(self, widget):
            logging.Handler.__init__(self)
            self.setLevel(logging.DEBUG)
            self.widget = widget
            self.widget.config(state='disabled')
            self.widget.tag_config("INFO", foreground="black")
            self.widget.tag_config("DEBUG", foreground="grey")
            self.widget.tag_config("WARNING", foreground="orange")
            self.widget.tag_config("ERROR", foreground="red")
            self.widget.tag_config("CRITICAL", foreground="red", underline=1)
    
            self.red = self.widget.tag_configure("red", foreground="red")
        def emit(self, record):
            self.widget.config(state='normal')
            # Append message (record) to the widget
            self.widget.insert(tk.END, self.format(record) + '\n', record.levelname)
            self.widget.see(tk.END)  # Scroll to the bottom
            self.widget.config(state='disabled') 
            self.widget.update() # Refresh the widget
    
    0 讨论(0)
提交回复
热议问题