34

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 the logging to anything other than a log file.

6 Answers 6

23

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

Sign up to request clarification or add additional context in comments.

1 Comment

For those of you getting the TclStackFree: incorrect freePtr error, the answer above solves this. Using self.widget.after(0, function_to_execute) ensures that the widget is modified by the thread it belongs to.
10

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')

Comments

10

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.

1 Comment

As Geeklhem's code, here missing self.widget.update() which leads frozen whole UI while appending log from other module
2

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)

Comments

2

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

Comments

1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.