1

In the process of creating a GUI for one of my command-line tools that allows for logging to a Tkinter ScrolledText widget, in addition to console or an external file.

I've successfully adapted the ScrolledText code from this thread on logging to a Tkinter text widget with logs being returned both to file and the GUI, but while logging to the console or file occurs in real-time, the ScrolledText widget isn't updated until the main() function has returned. Naturally, I'd like to see real-time updates in the Tkinter GUI as well.

import argparse
import logging
from os.path import basename, splitext, join, isfile
import time
import Tkinter as tk
import ScrolledText

class TextHandler(logging.Handler):
    """This class allows you to log to a Tkinter Text or ScrolledText widget"""

    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 GUI(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.generate_button = tk.Button(self.root, text="Generate", command=main)
        self.quit_button = tk.Button(self.root, text="Quit", command=self.root.quit)
        self.text_handler = None
        self.build_gui()

    def build_gui(self):
        self.root.title('Logging test')
        self.generate_button.grid(row=0, column=0)
        self.quit_button.grid(row=0, column=1)

        # Add ScrolledText widget to display logging
        st = ScrolledText.ScrolledText()
        st.configure(font='TkFixedFont')
        st.grid(row=1, sticky='ew', columnspan=2)

        # Create textlogger
        self.text_handler = TextHandler(st)


################################################################################
# Main Program Flow
#
def main():
    # log something
    logger.info('something to log')
    time.sleep(1)
    logger.info('and something one second later')
    time.sleep(1)
    logger.info('and yet another second...')


################################################################################
# Read commandline arguments
#
def get_arguments():
    parser = argparse.ArgumentParser(
        description="Test GUI logging")
    parser.add_argument('--logdir', required=False, default=None)
    parser.add_argument('--debug', action="store_const", const=logging.DEBUG, default=logging.INFO)
    a = parser.parse_args()
    return a


################################################################################
# Configure logger
#
def get_logger(log_level=logging.INFO, log_dir=None, text_handler=None):
    script = splitext(basename(__file__))[0]
    logg = logging.getLogger(script)
    logg.setLevel(log_level)

    # set up file or stdout handlers
    if log_dir:
        info_file = join(log_dir, script + '.log')
        info_handler = logging.FileHandler(info_file)
    else:
        info_handler = logging.StreamHandler()

    # create formatter and add it to the handlers
    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
    info_handler.setFormatter(formatter)
    info_handler.setLevel(log_level)
    if text_handler:
        text_handler.setFormatter(formatter)
        text_handler.setLevel(log_level)

    # add the handlers to logg
    logg.addHandler(info_handler)
    if text_handler:
        logg.addHandler(text_handler)

    return logg


################################################################################
# get args, configure logger and launch GUI
#
if __name__ == '__main__':
    args = get_arguments()
    root = tk.Tk()
    gui = GUI(root)
    logger = get_logger(log_level=args.debug, log_dir=args.logdir, text_handler=gui.text_handler)
    root.mainloop()
chambersisms
  • 41
  • 2
  • 7
  • I haven't analyzed your program closely, but I suspect that [`threading`](https://docs.python.org/3/library/threading.html) could be useful here. However, it can be a little tricky to do threading properly with Tkinter, since it likes to live in the main thread. But it's certainly possible: I've used threading to make a Tkinter program that displays images that get sent to it via a [`Queue`](https://docs.python.org/3/library/queue.html#module-queue). – PM 2Ring Apr 29 '18 at 14:43
  • Thanks @2Ring - there's a pretty thorough example of [threading/Queue logging to a ScrolledText widget here](https://beenje.github.io/blog/posts/logging-to-a-tkinter-scrolledtext-widget/), but as I'm not using threads, I figured the overhead was unnecessary. Happy to implement it, if necessary, but also curious as to why I'm not seeing anything hit the GUI until main() returns :) – chambersisms Apr 29 '18 at 15:10
  • It's because if main is running nothing else will run in tkinter loop until it returns. You are stuck in there and main sleeps that block the tkinter GUI. Beyond that I think there is also a problem with the `after` function registration. If you want to run append periodically, `append` should re-register itself with after in its last instruction. – progmatico Apr 29 '18 at 15:21
  • Thanks @progmatico - so sounds like I do need to implement threading + Queue to allow for main() to drop log messages on the Queue with tkinter mainloop() listening and updating the GUI? Will give that a shot. – chambersisms Apr 29 '18 at 15:37
  • I don't know if you do. You can have background processing in tkinter only with after, without using threads. See https://stackoverflow.com/questions/47943951/tkinter-code-runs-without-mainloop-update-or-after/47955106#47955106, look for the after part (and no sleep part). Also it is not clear to me if main shouldn't be a GUI method instead of a free function. – progmatico Apr 29 '18 at 15:48

0 Answers0