The program below simply tests whether I can call root.destroy() of tkinter in a child thread. Though no error raised, I see a lot of posts in stackoverflow reminding people not to use tkinter in a child thread, so I wonder if calling root.destroy() in a child thread is not a good idea too, though I used tkinter in the main thread?
import tkinter
from tkinter import ttk
import time
import _thread
def callBackFunc():
global LOOP_ACTIVE
LOOP_ACTIVE = False
root = tkinter.Tk()
root.geometry('200x100')
chkValue = tkinter.BooleanVar()
chkValue.set(False)
chk = ttk.Checkbutton(root, variable=chkValue, text='Click me', command=callBackFunc)
chk.grid(column=0, row=0)
def loop_function():
global LOOP_ACTIVE
LOOP_ACTIVE = True
while LOOP_ACTIVE:
print(chk.state())
time.sleep(5)
print("you clicked me")
root.destroy()
_thread.start_new_thread(loop_function, ())
root.mainloop()
print("printed after mainloop ends")
You can avoid calling destroy() in the child thread by periodically checking the flag in the main GUI thread by using the universal after() widget method to schedule them to occur.
In the code below I've added a check_loop_status() function that does that, and it will call root.destroy() if it discovers that the LOOP_ACTIVE flag is no longer set. To keep on process going, it schedules another call to itself before returning (which of course will never happen if destroy() got called).
Since this all that happens in the main thread, any call to destroy() will be fine.
import tkinter
from tkinter import ttk
import time
import _thread
DELAY = 1000 # milliseconds
root = tkinter.Tk()
root.geometry('200x100')
chkValue = tkinter.BooleanVar(value=False)
def callBackFunc():
global LOOP_ACTIVE
print("button was clicked")
LOOP_ACTIVE = False
def check_loop_status():
global LOOP_ACTIVE
if not LOOP_ACTIVE:
root.destroy() # OK in this function.
root.after(DELAY, check_loop_status) # Check again after delay.
chk = ttk.Checkbutton(root, variable=chkValue, text='Click me', command=callBackFunc)
chk.grid(column=0, row=0)
def loop_function():
global LOOP_ACTIVE
while LOOP_ACTIVE:
print(f'Checkbutton state: {chk.state()}')
time.sleep(5)
LOOP_ACTIVE = True
_thread.start_new_thread(loop_function, ())
check_loop_status() # Start periodic checking.
root.mainloop()
print("printed after mainloop ends")
Related
I am using tkinter to build my application for testing something. I have a start button which starts a function myFunc() which is defined something like this:
def myFunc():
for i in range(20):
# do something which takes 0.5sec to execute
And I have another button which should stop this function when pressed, even if its not fully executed.
There are 2 problems here. First, the loop in myFunc() takes 0.5*20=10seconds to execute, which will freeze my application for that long. I can use threading to overcome this but the second problem is if I use someThread.join() as the command for the stop button, it will still wait until the function is executed. Also, although the GUI is not frozen while myFunc() is running, it hangs when I press stop button (because now its waiting until the function completes). So I need to kill the thread and stop executing that function AS SOON AS the stop button is pressed. This is a necessity.
I read everywhere on the internet that its not recommended to kill a thread. How do I do this elegantly? I dont want any problems or freezing. I just want the function to stop executing myFunc() the instant I press the stop button.
Full source code:
import tkinter as tk
from tkinter import ttk
import time
from threading import Thread
def myFunc():
for i in range(20):
time.sleep(0.5)
print('DONE')
def start():
someThread.start()
def stop():
someThread.join()
root.quit()
root = tk.Tk()
root.title('My app')
someThread = Thread(target=myFunc)
ttk.Button(root, text='Start', command=start).pack()
ttk.Button(root, text='Stop', command=stop).pack()
root.mainloop()
We would need to define a variable such as stopped which will tell the code if to stop the for loop or not. I have also added print('hi') and print('hi') to check if def stop() is triggered or not and if the for loop stopped or not.
Code
import tkinter as tk
from tkinter import ttk
import time
from threading import Thread
def myFunc():
global stopped
for i in range(20):
print('hi')
if stopped == True: #checks if stopped is True or False
break
time.sleep(.5)
print('DONE')
def start():
someThread.start()
def stop():
global stopped
stopped = True #makes stopped True which will stop the for loop/thread
print('triggered')
root = tk.Tk()
root.title('My app')
someThread = Thread(target=myFunc)
ttk.Button(root, text='Start', command=start).pack()
ttk.Button(root, text='Stop', command=stop).pack()
stopped = False #stopped is false so the for loop can run
root.mainloop()
hi and triggered
hi
hi
hi
triggered #pressed stop
hi
DONE
I'm working on a webscraping code, and the process is quite long. Thus, I would like to start and stop it in a fancy way, with a button using tkinter. But I couldn't find any solution on the web.
The .after() function doesn't seems to be a good option as I don't want to restart my loop each time. My loop is already working based on urls readed from another document and parsing for each.
I tried the thread option with .join(). But couldn't manage to solve my problem with it. But maybe I don't write my code correctly.
My problem can be formulate as follow:
Start → Read urls doc and launch parsing loop.
Stop → Stop parsing and save content.
For instance I would like something like the following code to work:
from tkinter import *
flag=False
def test_loop():
while flag==True:
print("True")
while flag==False:
print("False")
def start():
global flag
flag=True
test_loop()
def stop():
global flag
flag=False
test_loop()
root = Tk()
root.title("Test")
root.geometry("500x500")
app = Frame(root)
app.grid()
start = Button(app, text="Start", command=start)
stop = Button(app, text="Stop", command=stop)
start.grid()
stop.grid()
root.mainloop()
Just do this:
while links and not stopped:
# scraping here
You can control the stopped bool flag with your button.
You need to import threading library as python takes only one command at a time so threading will create multiple threads to handle multiple tasks simultaneously.
This is the updated code:
from tkinter import *
import threading
flag=False
def test_loop():
while flag==True:
print("True")
if flag==False:
print("False")
def start():
global flag
flag=True
thread.start() #thread start
def stop():
global flag
flag=False
thread.join() #thread stop
#this is the point where you will create a thread
thread=threading.Thread(target=test_loop)
root = Tk()
root.title("Test")
root.geometry("500x500")
app = Frame(root)
app.grid()
start = Button(app, text="Start", command=start)
stop = Button(app, text="Stop", command=stop)
start.grid()
stop.grid()
root.mainloop()
Ok I managed to solve the problem, using a flag system indeed.
from tkinter import *
import threading
def st1():
global go1
global go2
go1=True
while go1==True:
go2=False
print('Start')
def st2():
global go1
global go2
go2=True
while go2==True:
go1=False
print('Stop')
def thread1():
threading.Thread(target=st1).start()
def thread2():
threading.Thread(target=st2).start()
root = Tk()
root.title("Test")
root.geometry("500x500")
app = Frame(root)
app.grid()
start = Button(app, text="Start", command=thread1)
stop = Button(app, text="Stop", command=thread2)
start.grid()
stop.grid()
root.mainloop()
Thanks for your help it was helpfull.
I have went through multiple solutions on the net, but they require a lot of code that might get confusing once you scale up. Is there a simple way to stop the thread and avoid the RuntimeError: threads can only be started once, in order to call the thread an infinite number of times. Here is a simple version of my code:
import tkinter
import time
import threading
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()
print(entry.get())
button = tkinter.Button(mainwindow, text="Press me", command=threading.Thread(target=func).start)
button.pack()
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, sys
class 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, time
def 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()
I've heard that threads in Python are not easy to handle and they become more tangled with tkinter.
I have the following problem. I have two classes, one for the GUI and another for an infinite process. First, I start the GUI class and then the infinite process' class. I want that when you close the GUI, it also finishes the infinite process and the program ends.
A simplified version of the code is the following:
import time, threading
from tkinter import *
from tkinter import messagebox
finish = False
class tkinterGUI(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global finish
#Main Window
self.mainWindow = Tk()
self.mainWindow.geometry("200x200")
self.mainWindow.title("My GUI Title")
#Label
lbCommand = Label(self.mainWindow, text="Hello world", font=("Courier New", 16)).place(x=20, y=20)
#Start
self.mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
class InfiniteProcess(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global finish
while not finish:
print("Infinite Loop")
time.sleep(3)
GUI = tkinterGUI()
GUI.start()
Process = InfiniteProcess()
Process.start()
When I click in the close button (in the upper right corner) the following error appears in the console:
Tcl_AsyncDelete: async handler deleted by the wrong thread
I don't know why it happens or what it means.
All Tcl commands need to originate from the same thread. Due to tkinter's
dependence on Tcl, it's generally necessary to make all tkinter gui statements
originate from the same thread. The problem occurs because
mainWindow is instantiated in the tkinterGui thread, but -- because mainWindow is an attribute of tkinterGui -- is not destroyed until tkinterGui is destroyed in the main thread.
The problem can be avoided by not making mainWindow an attribute of tkinterGui
-- i.e. changing self.mainWindow to mainWindow. This allows mainWindow to be destroyed when the run method ends in the tkinterGui thread. However, often you can avoid threads entirely by using mainWindow.after calls instead:
import time, threading
from tkinter import *
from tkinter import messagebox
def infinite_process():
print("Infinite Loop")
mainWindow.after(3000, infinite_process)
mainWindow = Tk()
mainWindow.geometry("200x200")
mainWindow.title("My GUI Title")
lbCommand = Label(mainWindow, text="Hello world", font=("Courier New", 16)).place(x=20, y=20)
mainWindow.after(3000, infinite_process)
mainWindow.mainloop()
If you want to define the GUI inside a class, you can still do so:
import time, threading
from tkinter import *
from tkinter import messagebox
class App(object):
def __init__(self, master):
master.geometry("200x200")
master.title("My GUI Title")
lbCommand = Label(master, text="Hello world",
font=("Courier New", 16)).place(x=20, y=20)
def tkinterGui():
global finish
mainWindow = Tk()
app = App(mainWindow)
mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
def InfiniteProcess():
while not finish:
print("Infinite Loop")
time.sleep(3)
finish = False
GUI = threading.Thread(target=tkinterGui)
GUI.start()
Process = threading.Thread(target=InfiniteProcess)
Process.start()
GUI.join()
Process.join()
or even simpler, just use the main thread to run the GUI mainloop:
import time, threading
from tkinter import *
from tkinter import messagebox
class App(object):
def __init__(self, master):
master.geometry("200x200")
master.title("My GUI Title")
lbCommand = Label(master, text="Hello world",
font=("Courier New", 16)).place(x=20, y=20)
def InfiniteProcess():
while not finish:
print("Infinite Loop")
time.sleep(3)
finish = False
Process = threading.Thread(target=InfiniteProcess)
Process.start()
mainWindow = Tk()
app = App(mainWindow)
mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
Process.join()
The fix here is simple, but hard to discover:
Call mainWindow.quit() immediately after mainwindow.mainloop(), so that the cleanup happens on the same thread as the one that created the tk UI, rather than on the main thread when python exits.
When my program executes the python GUI freezes. Here is my main code. Can I get some help in doing threading? So the execution happens in the background and I can still be able to use the "x" button in the GUI if I want to end the execution? Currently I just ask the user to close the cmd to end the program.
if __name__ == "__main__":
root = Tk()
root.title('Log')
root.geometry("400x220")
font1=('times', 15)
font2=('times', 10)
#Label inside root
Label(root, relief=GROOVE, font=font2, text="level").pack()
variable = StringVar(root)
variable.set("INFO") # default value
w = OptionMenu(root, variable, "CRITICAL", "DEBUG")
w.pack()
Button(root, font=font1, background= "yellow", text='START',command=main).pack()
Label(root, text="To end just close the CMD window").pack()
root.mainloop()
UPDATE: Turns out the Button callback was autorunning launch because the function object wasn't being set as the callback, the called function itself was. The fix is to replace the callback lambda: spawnthread(fcn) so that a function object is set as the callback instead. The answer has been updated to reflect this. Sorry for missing that.
The GUI mainloop will freeze when you try to run some other function, and has no way to restart itself (because it's frozen.)
Let's say the command you'd like to run alongside the GUI mainloop is myfunction.
Imports:
import time
import threading
import Queue
You need to set up a ThreadedClient class:
class ThreadedClient(threading.Thread):
def __init__(self, queue, fcn):
threading.Thread.__init__(self)
self.queue = queue
self.fcn = fcn
def run(self)
time.sleep(1)
self.queue.put(self.fcn())
def spawnthread(fcn):
thread = ThreadedClient(queue, fcn)
thread.start()
periodiccall(thread)
def periodiccall(thread):
if(thread.is_alive()):
root.After(100, lambda: periodiccall(thread))
You then want the widget calling the function to instead call a spawnthread function:
queue = Queue.Queue()
Button(root, text='START',command=lambda: spawnthread(myfunction)).pack() #<---- HERE
N.B. I'm adapting this from a multithreaded tkinter GUI I have; I have all my frames wrapped up in classes so this might have some bugs since I've had to tweak it a bit.