What is the best way to stop a thread and avoid 'RuntimeError' in python using threading and tkinter modules?
It is possible to call modifications on tkinter widgets from other threads, and they will occur as soon as the main thread is available, which may be immediately. If the background thread calling the modifications sleeps while the main thread is only in mainloop
, we can simulate a pause in the app without blocking on the main thread as the question aims for.
Then we can subclass Thread
to produce a thread that runs its own loop and remains started
even after its target finishes so that we can call its target as many times as we like. We can even pass errors that occur on the background thread through and gracefully exit the thread without hanging the app by using daemon
mode and a try
-except
block.
The BooleanVar
thread.do
acts as a switch that we can set in a lambda
to run func
once on the thread
when button
is pressed. This implements a cheap messaging system between the main and background threads which we could extend with little extra code to allow calling func
with arguments and returning values from it.
import threading, time, tkinter, sysclass ImmortalThread(threading.Thread): def __init__(self, func): super().__init__(daemon=True) self.func = func self.do = tkinter.BooleanVar() def run(self): while True: if self.do.get(): try: self.func() self.do.set(False) except: print("Exception on", self, ":", sys.exc_info()) raise SystemExit() else: # keeps the thread running no-ops so it doesn't strip time.sleep(0.01)def func(): entry.config(state='disabled') label.configure(text="Standby for seconds") time.sleep(3) sum = 0 for i in range(int(entry.get())): time.sleep(0.5) sum = sum+i label.configure(text=str(sum)) entry.config(state='normal')mainwindow = tkinter.Tk()mainwindow.title("Sum up to any number")entry = tkinter.Entry(mainwindow)entry.pack()label = tkinter.Label(mainwindow, text="Enter an integer", font=("Arial", 33))label.pack()thread = ImmortalThread(func)thread.start()button = tkinter.Button(mainwindow, text="Press me", command=lambda: thread.do.set(True))button.pack()mainwindow.mainloop()
While this is a simple way that get things done, in a fewer lines of code. I couldn't use the .join()
method. But, it seems that the app isn't creating any new threads. This is obvious through threading.active_counts()
method. Here is the code below:
import tkinter, threading, timedef calc(): entry.config(state='disabled') label.configure(text="Standby for 3 seconds") time.sleep(3) sum = 0 for i in range(int(entry.get())): time.sleep(0.5) sum = sum+i labelnum.configure(text=str(sum)) button.config(state='normal') label.configure(text="Sum up to any number") entry.config(state='normal')def func(): t = threading.Thread(target=calc) t.start() #del t print('Active threads:',threading.active_count())mainwindow = tkinter.Tk()mainwindow.title('Sum up to any number')entry = tkinter.Entry(mainwindow)entry.pack()label = tkinter.Label(mainwindow, text = "Enter an integer",font=("Arial",33))label.pack()labelnum = tkinter.Label(mainwindow, text="",font=('Arial',33))labelnum.pack()button = tkinter.Button(mainwindow, text="Press me", command=func)button.pack()mainwindow.mainloop()