1

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

PCM
  • 2,881
  • 2
  • 8
  • 30
Min_T
  • 29
  • 4
  • 1
    Tkinter has its own "thread", so starting another thread will stop it from running. BTW what are you trying to do? – PCM Nov 03 '21 at 08:04
  • Why do you call `win.after(...)` inside `set_text()`? – acw1668 Nov 03 '21 at 08:10
  • @PCM, without thread can make a hang. Cannot resize and untouchable of windows which has been popup. – Min_T Nov 03 '21 at 09:43
  • don't call `tkinter` stuff from other threads – Matiiss Nov 03 '21 at 09:43
  • @acw1668 I would make a loop infinitely ( loop: listen input--> change label ---> listen input – Min_T Nov 03 '21 at 09:45
  • You have already a while loop, so `win.after(...)` should be removed. – acw1668 Nov 03 '21 at 09:55
  • why do you want to use `input` instead of the `Entry` widget? `input` blocks execution but the issue is that simply closing the window won't stop the program, it will continue to wait for one more input in this case and when it is entered it will raise an exception because the `tkinter` widget updating after that will be destroyed – Matiiss Nov 03 '21 at 11:07

2 Answers2

3

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()
Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
  • you can create a separate `.after` "loop" that would for example get the last item in a list that the thread appends to and then change config text to that string (also implement some clearing mechanism so that the list doesn't fill up or use `queue.Queue`) – Matiiss Nov 03 '21 at 09:45
  • Excuse me! I dont have enough the reputation for thump it up. Once again, thanks alot! – Min_T Nov 03 '21 at 09:50
  • @Beginner I would appreciate if you set the [check mark](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work/5235#5235) on this answer if it solves your question. – Thingamabobs Nov 03 '21 at 09:52
  • this also raises an exception if you close the window and do anything with the input – Matiiss Nov 03 '21 at 11:08
0

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()
Matiiss
  • 5,970
  • 2
  • 12
  • 29