I want to come out of the loop immediately after pressing the stop button. But with this code, i could able to come out only after executing current iteration and next iteration.
It is very important for my application since iam going to use this for automating instruments, where operations have to be stopped immediately after pressing stop button.
# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 17:01:12 2018
#author: Lachu
"""
import time
from tkinter import *
from tkinter import ttk
root=Tk()
def start():
global stop_button_state
for i in range(1,20):
if (stop_button_state==True):
break
else:
print('Iteration started')
print('Iteration number: ', i)
root.update()
time.sleep(10)
print('Iteration completed \n')
def stop_fun():
global stop_button_state
stop_button_state=True
start=ttk.Button(root, text="Start", command=start).grid(row=0,column=0,padx=10,pady=10)
p=ttk.Button(root, text="Stop", command=stop_fun).grid(row=1,column=0)
stop_button_state=False
root.mainloop()
It's generally not a good idea to use time.sleep with GUI programs because it puts everything to sleep, so the GUI can't update itself, or respond to events. Also, it gets messy when you want to interrupt sleep.
I've adapted your code to use a Timer from the threading module. We can easily interrupt this Timer instantly, and it doesn't block the GUI.
To make this work, I moved your counting for loop into a generator.
If you press the Start button while a count is in progress it will tell you that it's already counting. When a count cycle is finished, either by pressing Stop, or by getting to the end of the numbers, you can press Start again to start a new count.
import tkinter as tk
from tkinter import ttk
from threading import Timer
root = tk.Tk()
delay = 2.0
my_timer = None
# Count up to `hi`, one number at a time
def counter_gen(hi):
for i in range(1, hi):
print('Iteration started')
print('Iteration number: ', i)
yield
print('Iteration completed\n')
# Sleep loop using a threading Timer
# The next `counter` step is performed, then we sleep for `delay`
# When we wake up, we call `sleeper` to repeat the cycle
def sleeper(counter):
global my_timer
try:
next(counter)
except StopIteration:
print('Finished\n')
my_timer = None
return
my_timer = Timer(delay, sleeper, (counter,))
my_timer.start()
def start_fun():
if my_timer is None:
counter = counter_gen(10)
sleeper(counter)
else:
print('Already counting')
def stop_fun():
global my_timer
if my_timer is not None:
my_timer.cancel()
print('Stopped\n')
my_timer = None
ttk.Button(root, text="Start", command=start_fun).grid(row=0, column=0, padx=10, pady=10)
ttk.Button(root, text="Stop", command=stop_fun).grid(row=1,column=0)
root.mainloop()
You are probably better off using root.after than threads:
In any events, as other pointed out, using time.sleep is a bad idea in a GUI.
You should also not name your buttons the same as your functions.
calling root.update, is also not necessary here.
from tkinter import *
from tkinter import ttk
def start_process(n=0, times=10):
n += 1
if not stop_button_state and n < times:
print('Iteration started')
print(f'Iteration number: {n}')
print('Iteration completed \n')
root.after(1000, start_process, n)
else:
print('stopping everything')
def stop_fun():
global stop_button_state
stop_button_state = True
if __name__ == '__main__':
root = Tk()
start = ttk.Button(root, text="Start", command=start_process)
start.grid(row=0, column=0, padx=10, pady=10)
p = ttk.Button(root, text="Stop", command=stop_fun)
p.grid(row=1, column=0)
stop_button_state = False
root.mainloop()
Without using a separate thread, you could always iterate over the sleep command, which would make the code more responsive.
e.g. This would reduce your wait between clicking stop and loop exit to 1/10th of a second, whilst retaining a 10 second gap between loops.
# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 17:01:12 2018
#author: Lachu
"""
import time
from tkinter import *
from tkinter import ttk
root=Tk()
stop_button_state=False
def start():
global stop_button_state
for i in range(1,20):
if (stop_button_state==True):
break
print('Iteration started')
print('Iteration number: ', i)
for i in range(100):
root.update()
time.sleep(0.1)
if (stop_button_state==True):
break
print('Iteration completed \n')
def stop_fun():
global stop_button_state
stop_button_state=True
ttk.Button(root, text="Start", command=start).grid(row=0,column=0,padx=10,pady=10)
ttk.Button(root, text="Stop", command=stop_fun).grid(row=1,column=0)
root.mainloop()
Related
I tried making a fast reaction tester with Tkinter Module in Python, but when I clicked the Start button, it justs freezes the window. And I don't know how to recover that
Here's my code:
import webbrowser as wb
import time
import math
from random import *
from tkinter import *
from PIL import ImageTk, Image
seconds = 0
miliseconds = 0
minutes = 0
def reactionStarted():
global seconds, miliseconds, greenimages, redimages, minutes
# Put Image Green
reactionImage.config(image=greenimages)
# Random Countdown
countdownSecond = randint(4, 9)
countdownMiliSecond = randint(0, 9)
# Turn into float ( More Randomized )
countdownBonk = float(str(countdownSecond) + "." + str(countdownMiliSecond))
# Start Countdown
print(countdownBonk) # i was testing if this was the problem but its not
time.sleep(countdownBonk)
# Red image ( fast reaction part )
reactionImage.config(image=redimages)
# Timer
timeLoop = True
while timeLoop:
miliseconds += 1
time.sleep(0.1)
if miliseconds == 10:
seconds += 1
miliseconds = 0
elif seconds == 60:
seconds = 0
minutes += 1
def reactionCompleted():
global seconds, miliseconds, minutes
timeLoop = False
if not timeLoop:
reactionImage.config(image='', text=(
str(minutes) + "Minute(s)" + str(seconds) + "Second(s)" + str(miliseconds) + "Milisecond(s)"))
root = Tk()
root.title("Fast Reaction Test")
greenimages = ImageTk.PhotoImage(Image.open("green.png"))
redimages = ImageTk.PhotoImage(Image.open("red.png"))
reactionImage = Label(text='Click the button Below To Start!')
reactionImage.pack()
Start = Button(root, width=500, height=5, text="Click Here to Start", command=reactionStarted)
Start.pack()
Stop = Button(root, width=500, height=10, text="Stop (Spacebar)", command=reactionCompleted)
Stop.bind("<space>", reactionCompleted)
Stop.focus_force()
Stop.pack()
root.mainloop()
Really, thanks if you helped me out :)
Your error is that you are asking your program to enter an infinite loop when clicking the start button. The interpreter never leaves that loop, and thus the UI gets stuck without being able to update itself or receive input, because the interpreter is still stuck within your loop.
So you need to have another approach for this. Issues like these are normally handled by opening separate threads in your program, that can execute independently such that your main thread responsible for updating the UI window is not impacted by the child thread running your infinite loop. Then the main thread can at some point send a message to the child thread that the loop should be cancelled, when you user presses the stop button.
Handling this in tkinter has been made easy with the after() method, which simply put creates such an infinite loop in a separate thread to allow the main thread to keep running. I have below included a small example of how such an after loop can look, and you can try implementing that in your own code. If you still have problems, open a new question with more clarity.
import tkinter as tk
import time
class Timer:
def __init__(self):
self.root = tk.Tk()
self.sv = tk.StringVar()
self.start_time = None
self.after_loop = None
self.make_widgets()
self.root.mainloop()
def make_widgets(self):
tk.Label(self.root, textvariable=self.sv).pack()
tk.Button(self.root, text='start', command=self.start).pack()
tk.Button(self.root, text='stop', command=self.stop).pack()
def start(self):
self.start_time = time.time()
self.timer()
def timer(self):
self.sv.set(round(time.time() - self.start_time))
self.after_loop = self.root.after(500, self.timer)
def stop(self):
if self.after_loop is not None:
self.root.after_cancel(self.after_loop)
self.after_loop = None
Timer()
I've created an example code, so I could be more specific. Actual code is much more complex but kinda similar in functionality I want for stop button.
The code I can't figure out will go in line 25.
import threading
from tkinter import *
import time
import concurrent.futures
window = Tk()
window.geometry('400x300')
def main_fctn():
for i in range(500):
print(f'Counting {i}')
time.sleep(2)
def count_threads():
print(f'\n\nCurrently running threads: {threading.activeCount()}\n\n'.upper())
def starting_thread(arg=None):
with concurrent.futures.ThreadPoolExecutor() as excecuter:
thread_list = excecuter.submit(main_fctn)
def stop_threads():
### CODE TO STOP THE RUNNING THREADS
pass
button1 = Button(window, text='Start Thread!')
button1.bind('<Button-1>', lambda j: threading.Thread(target=starting_thread).start())
button1.pack(pady=25)
button2 = Button(window, text='Stop Thread!')
button2.bind('<Button-1>', lambda j: threading.Thread(target=stop_threads).start())
button2.pack(pady=25)
button3 = Button(window, text='Count number of Threads')
button3.bind('<Button-1>', lambda j: threading.Thread(target=count_threads).start())
button3.pack(pady=25)
window.mainloop()
Simple way is to use threading.Event() object:
create an instance of threading.Event() in global space
call clear() on the object before the for loop inside main_fctn()
check whether the object is set using is_set() inside the for loop, if it is set, break the for loop
call set() on the object inside stop_threads() if you want to stop the thread
Below is an example based on yours:
import threading
from tkinter import *
import time
import concurrent.futures
window = Tk()
window.geometry('400x300')
# event object for stopping thread
event_obj = threading.Event()
def main_fctn():
event_obj.clear() # clear the state
for i in range(500):
if event_obj.is_set():
# event object is set, break the for loop
break
print(f'Counting {i}')
time.sleep(2)
print('done')
def count_threads():
print(f'\n\nCurrently running threads: {threading.activeCount()}\n\n'.upper())
def starting_thread(arg=None):
with concurrent.futures.ThreadPoolExecutor() as excecuter:
thread_list = excecuter.submit(main_fctn)
def stop_threads():
### CODE TO STOP THE RUNNING THREADS
event_obj.set()
button1 = Button(window, text='Start Thread!')
button1.bind('<Button-1>', lambda j: threading.Thread(target=starting_thread).start())
button1.pack(pady=25)
button2 = Button(window, text='Stop Thread!')
button2.bind('<Button-1>', lambda j: threading.Thread(target=stop_threads).start())
button2.pack(pady=25)
button3 = Button(window, text='Count number of Threads')
button3.bind('<Button-1>', lambda j: threading.Thread(target=count_threads).start())
button3.pack(pady=25)
window.mainloop()
When using the Tkinter .after method, the code continues passed without waiting for the callback to complete.
import tkinter as tk
import tkinter.ttk as ttk
import time
from datetime import datetime
global i
i = 0
global j
j = 0
def SomeFunction():
global i
for num in range(10):
i+=1
x = barVar.get()
barVar.set(x+5)
histrun_mainWindow.update()
time.sleep(2)
def SecondFunction():
global j
for num in range(10):
j+=1
x = barVar.get()
barVar.set(x+5)
histrun_mainWindow.update()
time.sleep(2)
def Load(run_date):
histrun_mainWindow.after(50, SomeFunction)
histrun_mainWindow.after(50, SecondFunction)
global i, j
print 'Number is :', i + j
histrun_mainWindow = tk.Tk()
run_date = datetime.today().date()
barVar = tk.DoubleVar()
barVar.set(0)
bar = ttk.Progressbar(histrun_mainWindow, length=200, style='black.Horizontal.TProgressbar', variable=barVar, mode='determinate')
bar.grid(row=1, column=0)
button= tk.Button(histrun_mainWindow, text='Run for this date ' + str(run_date), command=lambda:Load(run_date))
button.grid(row=0, column=0)
histrun_mainWindow.mainloop()
This example shows what is happening. The .after() calls the Load() function but doesn't wait for Load() to complete, it goes straight on to the next line.
I want i to print as 10 but because the .after() doesn't wait for Load() to finish it's additions, i prints as 0
The progress bar continues to update so I know that Load was called as it continues in the background after i is printed
Question: the progress bar doesn't update - the window freezes until all functions have completed
Use a Thread to prevent main loop from freezing.
Your functions - SomeFunction, SecondFunction - may also in global namespace.
Then you have to pass self.pbar as paramter, e.g. SomeFunction(pbar): ... f(self.pbar).
Note:
You see a RuntimeError: main thread is not in main loop
if you .destroy() the App() window while the Thread is running!
import tkinter as tk
import threading
class App(tk.Tk):
def __init__(self):
super().__init__()
btn = tk.Button(self, text='Run',
command=lambda :threading.Thread(target=self.Load).start())
btn.grid(row=0, column=0)
self.pbar = ttk.Progressbar(self, maximum=2 *(5 * 5), mode='determinate')
self.pbar.grid(row=1, column=0)
def SomeFunction(self):
for num in range(5):
print('SomeFunction({})'.format(num))
self.pbar['value'] += 5
time.sleep(1)
return num
def SecondFunction(self):
for num in range(5):
print('SecondFunction({})'.format(num))
self.pbar['value'] += 5
time.sleep(1)
return num
def Load(self):
number = 0
for f in [self.SomeFunction, self.SecondFunction]:
number += f()
print('Number is :{}'.format(number))
if __name__ == "__main__":
App().mainloop()
Output:
SomeFunction(0)
SomeFunction(1)
SomeFunction(2)
SomeFunction(3)
SomeFunction(4)
SecondFunction(0)
SecondFunction(1)
SecondFunction(2)
SecondFunction(3)
SecondFunction(4)
Number is :8
Tested with Python: 3.5
*)Could not test with Python 2.7
I am having a problem where my Tkinter UI becomes completley stuck and non-interactive while a for loop is running. My example code print "Looping" while it is in the loop and there is a "Cancel" button on the UI which is supposed to stop the loop, but since I am unable to click the "Cancel" button the loop can not be stopped. So my question is how can I make my tkinter UI usable while a loop is running. Here is the example code:
from tkinter import*
import time
root = Tk()
i=10
flag = False
def loop():
flag = True
for i in range(100):
if flag == True:
time.sleep(0.5)
print("Looping")
def canc():
flag = False
btn = Button(root, text="Start Loop", command=loop).pack()
cncl = Button(root, text="Cancel", command=canc).pack()
root.mainloop()
I have tried creating a new thread for the loop function but this does not work.
Updated code, the UI is responsive, but nothing happens when cancel is pressed:
from tkinter import*
import threading
import time
root = Tk()
i=10
flag = False
def loop():
flag = True
for i in range(10):
if flag == True:
time.sleep(0.5)
print("Looping")
def run():
threading.Thread(target=loop).start()
def canc():
flag = False
btn = Button(root, text="Start Loop", command=run).pack()
cncl = Button(root, text="Cancel", command=canc).pack()
root.mainloop()
'flag' isnt a global variable so when it is set to False in canc(), the value of the 'flag' local variable in loop() isnt changed and the loop therefore isnt stopped
also root.update() needs to be used to update the GUI
Remedial actions:
from tkinter import*
import threading
import time
root = Tk()
def loop():
global flag
flag = True
for i in range(10):
if flag == True:
root.update()
time.sleep(0.5)
print("Looping")
def canc():
global flag
flag = False
btn = Button(root, text="Start Loop", command=loop).pack()
cncl = Button(root, text="Cancel", command=canc).pack()
root.mainloop()
I found a way around that problem:
I started my time-consuming job inside a thread and I checked if my thread is still running in a while loop, and inside that, I did update my Tkinter root.
here is my code:
def start_axis(input):
print(input)
time.sleep(5)
def axis():
t = threading.Thread(target=start_axis, args=("x"))
t.start()
while t.is_alive():
try:
root.update()
except:
pass
the args part is important so the thread doesn't call the function immediately
I am running a thread to constantly monitor temperature, and update a global variable that other functions call upon to perform their tasks accordingly. Everything works now, and I could live with it. But I wanted to reach out and see if anyone has a smarter way to keep this going until the mainloop ends. Here is how the thread is started...
thread1 = Thread(target = test)
try:
thread1.start()
except (KeyboardInterrupt, SystemExit):
thread1.stop()
sys.exit()
I guess I could make a small script just to let it run. Sorry...
import time
import tkinter
from tkinter import *
from threading import Thread
root = Tk()
timer = True
global counter
counter = 0
var1 = StringVar()
var1.set(counter)
def test():
global counter
while timer:
counter += 1
var1.set(counter)
root.update
time.sleep(1)
testtext = Label(root,textvariable=var1,font='Consolas 24 bold')
testtext.grid(row=1,column=1,sticky="N,S,E,W",padx=10,pady=0)
testtext2 = Label(root,text="SECONDS",font='Consolas 18 bold')
testtext2.grid(row=2,column=1,sticky="N,S,E,W",padx=10,pady=0)
thread1 = Thread(target = test)
try:
thread1.start()
except (KeyboardInterrupt, SystemExit):
thread1.stop()
sys.exit()
root.mainloop()
As you will see, after you close the window, the thread does end, but not very cleanly.
Any ideas?
Tkinter supports protocol handlers.
The most commonly used protocol is called WM_DELETE_WINDOW, and is used to define what happens when the user explicitly closes a window using the window manager.
import time
import tkinter
from tkinter import *
from threading import Thread
def on_closing():
thread1.close()
root.destroy()
def test():
global counter
while timer:
counter += 1
var1.set(counter)
root.update
time.sleep(1)
root = Tk()
timer = True
global counter
counter = 0
var1 = StringVar()
var1.set(counter)
testtext = Label(root,textvariable=var1,font='Consolas 24 bold')
testtext.grid(row=1,column=1,sticky="N,S,E,W",padx=10,pady=0)
testtext2 = Label(root,text="SECONDS",font='Consolas 18 bold')
testtext2.grid(row=2,column=1,sticky="N,S,E,W",padx=10,pady=0)
global thread1 = Thread(target = test)
try:
thread1.start()
except (KeyboardInterrupt, SystemExit):
thread1.stop()
sys.exit()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
Basically, the program listens to the close event and decides what to do as the window is killed in the functuon on_closing().
Got it. Thank you Daniel Reyhanian for pointing me toward the
root.protocol("WM_DELETE_WINDOW", on_closing)
thing. Never seen that before.
Here is the final bit using: os._exit(1)
import os
import time
import tkinter
from tkinter import *
from threading import Thread
root = Tk()
timer = True
global counter
counter = 0
var1 = StringVar()
var1.set(counter)
def on_closing():
os._exit(1)
def test():
global counter
while timer:
counter += 1
var1.set(counter)
root.update
time.sleep(1)
testtext = Label(root,textvariable=var1,font='Consolas 24 bold')
testtext.grid(row=1,column=1,sticky="N,S,E,W",padx=10,pady=0)
testtext2 = Label(root,text="SECONDS",font='Consolas 18 bold')
testtext2.grid(row=2,column=1,sticky="N,S,E,W",padx=10,pady=0)
thread1 = Thread(target = test)
thread1.start()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()