Tkinter got freeze with threading

Question:

I have plan to make a simple test with tkinter which will change the left text with new input.
But it seems threading is not easy as my thought. It still be hang after 2 times of input.

from tkinter import *
import threading

win = Tk()
label1 = Label(win, text="this is a test on the left")
label1.pack(side=LEFT)
label2 = Label(win, text="this is a test on the right")
label2.pack(side=RIGHT)
def set_text():
    while(True):
        content=input("let enter the substuition:")
        label1.config(text = content)
        win.after(100, set_text)

setTextthr=threading.Thread(target = set_text)
setTextthr.start()

win.mainloop()

It is very impressed if you can point out why it happened and how to fix.
Thanks

Asked By: Beginner

||

Answers:

There are some unwritten rules about threading in combination with tkinter.
One of them is not to touch any widget outside of the thread of tkinter.
This line violates this rule.

label1.config(text = content)

In order to do this you can use a tkinter.StingVar which isn’t directly in the main event loop of tkinter.

The call from inside the function to itself with after some time will create a new stack of a while-loops on top. Below you find a working example which has its limits. For another approach you can take a look at this or this.

from tkinter import *
import threading

win = Tk()
var = StringVar(value="this is a test on the left")
label1 = Label(win,textvariable=var)
label1.pack(side=LEFT)
label2 = Label(win, text="this is a test on the right")
label2.pack(side=RIGHT)
def set_text():
    while(True):
        content=input("let enter the substuition:")
        var.set(content)

setTextthr=threading.Thread(target = set_text)
setTextthr.start()

win.mainloop()
Answered By: Thingamabobs

The usual way to approach getting data from threads in an event-driven program (as it is with tkinter) is to use some update loop (.after) to schedule a check on the data container (queue.Queue or a simple list) that contains data from the thread. Then additionally check if the thread is alive at all, and if it is not you can stop the update loop because there is nothing to update anymore. Also use some flag (threading.Event) to stop the thread loop upon destroying the window. The only issue is that input is used which makes it so that at the end at least the Enter key has to be pressed.

The code example (explanation in code comments):

import tkinter as tk
import threading
import queue


# this is the function that will run in another thread
def ask_input(q):  # argument is the queue
    # here check if the event is set, if it is
    # don't loop anymore, however it will still wait
    # for input so that has to be entered and
    # then the thread will stop
    while not event.is_set():
        user_input = input('New content: ')
        # put the user entered data into the queue
        q.put(user_input)


# the function that will update the label
def update_label(q, t):  # pass in the the queue and thread
    # this loop simply gets the last item in the queue
    # otherwise (not in this case perhaps) it may be a little
    # too slow in updating since it only runs every 100 ms
    # so it need to catch up
    while not q.empty():
        # get data from the queue
        text = q.get()
        # here it is save to update the label since it is the same
        # thread where all of the tkinter runs
        label.config(text=text)
    # check if thread still runs (in this case this specific check
    # is unnecessary since the thread is stopped only after
    # the main window is closed)
    if t.is_alive():
        # schedule the next call with the same queue and thread
        root.after(100, update_label, q, t)


# the function that initiates the thread
def start_thread():
    # create a queue to pass as the argument to the thread and updater
    _queue = queue.Queue()
    # create and start a thread
    thread = threading.Thread(target=ask_input, args=(_queue,))
    thread.start()
    # start updating the label
    update_label(_queue, thread)


# check if the code is run without importing
if __name__ == '__main__':
    root = tk.Tk()
    # set this attribute so that the window always stays on top
    # so that you can immediately see the changes in the label
    root.attributes('-topmost', True)
    # create the event
    event = threading.Event()
    # if window is destroyed set event which will break the loop
    root.bind('<Destroy>', lambda _: event.set())

    label = tk.Label(root)
    label.pack()

    start_thread()

    root.mainloop()
Answered By: Matiiss
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.