Reputation:
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.
Upvotes: 33
Views: 39989
Reputation: 1787
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.
Upvotes: 1
Reputation: 824
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
Upvotes: 22
Reputation: 689
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
Upvotes: 2
Reputation: 15380
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)
Upvotes: 2
Reputation: 11826
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.
Upvotes: 10
Reputation: 15154
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')
Upvotes: 10