Ma0
Ma0

Reputation: 15204

Python tkinter text widget for real-time logging

I am trying to create a GUI for my python app and I am struggling to get the Text Widget to behave like a stdout. More specifically, I want to the widget to display the information as soon as it is requested to do so and not at the end when everything is finished.

To demonstrate, consider this:

import sys
import time
import multiprocessing as mp
import multiprocessing.queues as mpq
from threading import Thread
from tkinter import *


class StdoutQueue(mpq.Queue):

    def __init__(self, *args, **kwargs):
        ctx = mp.get_context()
        super(StdoutQueue, self).__init__(*args, **kwargs, ctx=ctx)

    def write(self,msg):
        self.put(msg)

    def flush(self):
        sys.__stdout__.flush()
        
        
def text_catcher(text_widget,queue):
    while True:
        text_widget.insert(END, queue.get())


def test_child(q):
    sys.stdout = q
    print('child running')


def test_parent(q):
    sys.stdout = q
    print('parent running')
    time.sleep(5.)
    mp.Process(target=test_child,args=(q,)).start()


if __name__ == '__main__':
    gui_root = Tk()
    gui_txt = Text(gui_root)
    gui_txt.pack()
    q = StdoutQueue()
    gui_btn = Button(gui_root, text='Test', command=lambda: test_parent(q),)
    gui_btn.pack()

    monitor = Thread(target=text_catcher, args=(gui_txt, q))
    monitor.daemon = True
    monitor.start()

    gui_root.mainloop()

Hitting the Test button does not print 'parent running' right away and 'child running' 5 seconds later as desired, but rather, it waits 5 seconds and prints the two strings at once.

Is there any way to get the desired behaviour?

Upvotes: 1

Views: 810

Answers (2)

Lydia van Dyke
Lydia van Dyke

Reputation: 2516

I do not understand completely what is going on. text_catcher is being blocked when test_parent is called from the Tk thread. I would have assumed the monitor thread prevents this. Well...

One possible workaround could be to put test_parent in its own thread. This can be easily done with a small decorator:


def run_in_thread(fun):
    def wrapper(*args, **kwargs):
        thread = Thread(target=fun, args=args, kwargs=kwargs)
        thread.start()
        return thread

    return wrapper


@run_in_thread
def test_parent(q):
    sys.stdout = q
    print('parent running')
    time.sleep(5.)
    mp.Process(target=test_child, args=(q,)).start()

With test_parent wrapped like this, the text message appears immediatley.

Upvotes: 1

crackanddie
crackanddie

Reputation: 708

Just move time.sleep(5.) from test_parent into test_child:


def test_child(q):
    sys.stdout = q
    time.sleep(5.)
    print('child running')


def test_parent(q):
    sys.stdout = q
    print('parent running')
    mp.Process(target=test_child, args=(q,)).start()

Even when I try it without StdoutQueue it works fine, here is the code:

import time
import multiprocessing as mp
from tkinter import *


def test_child():
    time.sleep(5.)
    print('child running')


def test_parent():
    print('parent running')
    mp.Process(target=test_child, args=()).start()


if __name__ == '__main__':
    gui_root = Tk()
    gui_txt = Text(gui_root)
    gui_txt.pack()
    gui_btn = Button(gui_root, text='Test', command=lambda: test_parent(), )
    gui_btn.pack()
    gui_root.mainloop()

Upvotes: 0

Related Questions