TkInter: how to wait until callback from after method has completed - python

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

Related

Sharing variables between threads in Python tkinter

Is it possible to get the GUI to update variable 'a' to update in real time while 'thread2' increments the value?
import tkinter as tk
from threading import Thread
import time
a = 0 # global variable
def thread1(threadname):
root = tk.Tk()
w = tk.Label(root, text=a)
w.pack()
root.mainloop()
def thread2(threadname):
global a
while True:
a += 1
time.sleep(1)
thread1 = Thread( target=thread1, args=("Thread-1", ) )
thread2 = Thread( target=thread2, args=("Thread-2", ) )
thread1.start()
thread2.start()
If I create a loop and print 'a' I get the correct result.
def thread1(threadname):
global a
while True:
print(a)
# root = tk.Tk()
# w = tk.Label(root, text=a)
# w.pack()
# root.mainloop()
Any help would be appreciated
When you create your label, that's not a "live" connection. That passes the current value of the variable a. You then enter your main loop, and that thread does nothing else until the application exits. You need to send the new value to a function that executes as part of the main thread, and that function will need access to the label.
This works:
import tkinter as tk
from threading import Thread
import time
class GUI(object):
def __init__(self):
self.a = 0
self.w = tk.Label(root, text=self.a)
self.w.pack()
thread2 = Thread( target=self.thread2, args=("Thread-2", ) )
thread2.start()
def thread2(self,threadname):
while True:
self.a += 1
root.after_idle(self.update)
time.sleep(1)
def update(self):
self.w.config(text=self.a)
root = tk.Tk()
gui = GUI()
root.mainloop()
It is possible to make a live connection using textvariable, but then you have to change the type of a to a tkinter.StringVariable. Check here: Update Tkinter Label from variable
Try using a <tkinter.Tk>.after loop like this:
import tkinter as tk
def loop():
global a
a += 1
label.config(text=a)
# run `loop` again in 1000 ms
root.after(1000, loop)
a = 0
root = tk.Tk()
label = tk.Label(root, text=a)
label.pack()
loop() # Start the loop
root.mainloop()
I used a .after script because tkinter and threading don't go together very well. Sometimes tkinter can crash without even giving you an error message if you try to call some tkinter commands from the second thread.

tkinter mouse click counter won't break out of my countdown loop

I'm trying to write a little program in python using tkinter, to count the number of times the mouse clicks a button in 60 sec, but I have a problem: I can't break the countdown loop.
Below is my code:
from tkinter import *
class Application(Frame):
def __init__(self,master):
super(Application,self).__init__(master)
self.pack()
self.bttn_clicks = 0
self.createWidgets()
def createWidgets(self):
self.labelvariable = StringVar()
self.labelvariable.set("60")
self.thelabel = Label(self,textvariable = self.labelvariable,font=('Helvetica',50))
self.thelabel.pack(side=TOP)
self.firstButton = Button(self, text="Start", command=self.update_count)
self.firstButton.pack(side=TOP)
def update_count(self):
self.bttn_clicks += 1
self.firstButton["text"] = "Counter: " + str(self.bttn_clicks)
if self.bttn_clicks == 1:
countdown(1559)
def countdown(timeInSeconds):
mins, secs = divmod(timeInSeconds, 60)
timeformat = "{1:02d}".format(mins, secs)
app.labelvariable.set(timeformat)
root.after(1000, countdown, timeInSeconds-1)
if __name__ == '__main__':
root = Tk()
root.title("Timer")
app = Application(root)
root.mainloop()
What I would do is keep everything within the class. Using an outside function just makes things harder to manage.
That said you should use IntVar() instead of string var as this will aid us in keeping track of the time.
My below code will check first if the timer is at 60. If so then start count down and add to counter. When the counter reaches zero the button has its command disabled and it no longer adds to the counter.
The other thing I changed was adding a manager method for the timer. Because we are now using IntVar() all we need to do is a get() command followed by a -1 and an after() statement to keep the timer running until zero.
I also cleaned up your code a little to follow the PEP8 standard.
import tkinter as tk
class Application(tk.Frame):
def __init__(self,master):
super(Application,self).__init__(master)
self.bttn_clicks = 0
self.labelvariable = tk.IntVar()
self.create_widgets()
def create_widgets(self):
self.labelvariable.set(60)
self.thelabel = tk.Label(self, textvariable=self.labelvariable, font=('Helvetica',50))
self.thelabel.pack(side=tk.TOP)
self.firstButton = tk.Button(self, text="Start", command=self.update_count)
self.firstButton.pack(side=tk.TOP)
def update_count(self):
if self.labelvariable.get() == 60:
self.manage_countdown()
if self.labelvariable.get() != 0:
self.bttn_clicks += 1
self.firstButton.config(text="Counter: {}".format(self.bttn_clicks))
else:
self.firstButton.config(command=None)
def manage_countdown(self):
if self.labelvariable.get() != 0:
self.labelvariable.set(self.labelvariable.get() - 1)
self.after(1000, self.manage_countdown)
if __name__ == '__main__':
root = tk.Tk()
root.title("Timer")
app = Application(root).pack()
root.mainloop()

How to kill time.sleep() in python 3 while compiler is sleeping

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()

only last modification to Label being shown in Python - Tkinter . Why?

I am learning GUI development using Tkinter. I want to show multiple messages on the label which I have stored in a string. I used sleep to view the changes.However only the last message string is shown at execution.
from tkinter import *
import time
master = Tk()
def onClick():
for i in range(0,len(list_of_str)):
w.configure(text=list_of_str[i])
time.sleep(5)
list_of_str = ['first','second','third','fourth','fifth']
w = Label(master, text="Hello, world!")
b = Button(master,text='Click me',command = onClick)
w.pack()
b.pack()
mainloop()
I am a noobie. So thanks for helping !
A simple solution to your problem is to use a combination of the try/except method and using after().
In tkinter sleep() will pause the application instead of providing a timer. For tkinter you want to use the after() method to scheduled an event after a set amount of time instead. The after() method is meant for this exact problem and is what you will always use in tkinter for a delayed event.
In my below example I modified your onClick function to take 1 argument and to use that in our after() method to select the next item in the list after 5 seconds. Note that for the after() method time is done in milliseconds so 5000 is 5 seconds.
from tkinter import *
master = Tk()
def onClick(ndex):
try:
w.configure(text=list_of_str[ndex])
master.after(5000, onClick, ndex+1)
except:
print("End of list")
list_of_str = ['first','second','third','fourth','fifth']
w = Label(master, text="Hello, world!")
b = Button(master,text='Click me',command = lambda: onClick(0))
w.pack()
b.pack()
mainloop()
I think you want this:
from tkinter import *
import time
master = Tk()
global i
i = 0
def onClick():
master.after(1, change)
def change():
global i
if i == len(list_of_str):
pass
else:
w.configure(text=list_of_str[i])
i += 1
master.after(1000, onClick)
list_of_str = ['first','second','third','fourth','fifth']
w = Label(master, text="Hello, world!")
b = Button(master,text='Click me',command = onClick)
w.pack()
b.pack()
mainloop()
time.sleep is a no-no in tkinter. I advise you make your gui in a class and it wil be easier.
example with class:
import tkinter as tk
from tkinter import *
class GUI:
def __init__(self, master):
self.list_of_str = ['first','second','third','fourth','fifth']
self.count = 0
self.master = master
self.w = Label(master, text="Hello, world!")
self.w.pack()
self.b = Button(master,text='Click me',command = self.onClick)
self.b.pack()
def onClick(self, event=None):
if self.count == len(self.list_of_str):
pass
else:
self.w.configure(text=self.list_of_str[self.count])
self.count += 1
self.master.after(1000, self.onClick)
def main():
root = tk.Tk()
app = GUI(root)
root.mainloop()
if __name__ == '__main__':
main()

Label Is Not Shown When Called From Other Tkinter Program

I was writing a program with a start page, and two programs that are called from that start page. Both of the subprograms work by themselves. However, when I put them into my start page, the stopwatch timing label doesn't show up. If you are wondering, I put them into my program by doing:
import program
program.function()
Here is my start page program:
from Tkinter import *
class start_page:
def __init__(self,master):
self.master = master
self.frame = Frame(self.master)
self.countdown = Button(master, text = "Timer", command = self.c).pack()
self.stopwatch_butt = Button(master,text="Stopwatch",command=self.g).pack()
def g(self):
import stopwatch
stopwatch.f()
def c(self):
import timer_prog
timer_prog.timer()
self.master.after_cancel(timer_prog)
def main():
root = Tk()
s = start_page(root)
root.title("Timer Suite: Brian Ton")
root.mainloop()
main()
If I run this program, the timer program works fine, but the stopwatch doesn't show its label, only its buttons. I tried to clear all Tk after functions, and that didn't work, and I also tried to run the stopwatch program first, to no avail.
Here is my stopwatch program:
from Tkinter import *
import datetime
def s():
start.config(state='disabled')
stop.config(state="normal")
reset.config(state='disabled')
Start()
def Start():
if reset['state'] == 'disabled' and stop['state'] == 'normal':
hidden.set(str(int(hidden.get())+1))
root.update()
root.after(1000,Start)
curr = hidden.get()
g.set(str(datetime.timedelta(seconds=int(curr))))
print g.get()
else:
return None
def Stop():
start.config(state='disabled')
stop.config(state='disabled')
reset.config(state="normal")
def Reset():
start.config(state="normal")
stop.config(state="disabled")
reset.config(state='disabled')
hidden.set('0')
g.set(str(datetime.timedelta(seconds=0)))
def f():
global root,frame,master,hidden,g,timelabel,start,stop,reset
root = Tk()
frame = Frame(root)
master = root
hidden = StringVar()
g = StringVar()
hidden.set('0')
timelabel = Label(master,textvariable=g)
g.set(str(datetime.timedelta(seconds=int(0))))
timelabel.grid(row=1,column=2)
start = Button(master,text="Start",command = s,state="normal")
stop = Button(master,text="Stop",command = Stop,state = "disabled")
reset = Button(master,text="Reset",command = Reset,state = "disabled")
start.grid(row=2,column=1)
stop.grid(row=2,column=2)
reset.grid(row=2,column=3)
root.update()
root.mainloop()
And here is my timer program:
from Tkinter import *
import datetime
def get_seconds(h,m,s):
hr_sec = h * 3600
m_sec = m * 60
return hr_sec+m_sec+s
def timerstartstop():
hours = hour_entry.get()
minutes = minute_entry.get()
sec = second_entry.get()
if hours == "":
hours = 0
hour_entry.insert(0,"0")
if minutes == "":
minutes = 0
minute_entry.insert(0,"0")
if sec == "":
sec = 0
second_entry.insert(0,"0")
c = get_seconds(int(hours), int(minutes), int(sec))
global s
s = StringVar(master)
s.set(c)
if startstop['text'] == 'Stop':
global curr
curr = shown
s.set(-1)
if startstop['text'] == 'Reset':
startstop.config(text="Start")
s.set(c)
root.update()
shown.set(str(datetime.timedelta(seconds=int(s.get()))))
return None
countdown()
import winsound
def countdown():
startstop.config(text="Stop")
global shown
good = True
shown = StringVar(master)
shown.set(str(datetime.timedelta(seconds=int(s.get()))))
L = Label(master,textvariable=shown).grid(row=1,column=2)
if int(s.get()) == 0:
startstop.config(text="Reset")
while startstop['text'] != "Start":
root.update()
winsound.Beep(500,500)
elif int(s.get()) < 0:
good = False
shown.set(curr.get())
startstop.config(text="Reset")
else:
if good:
s.set(str(int(s.get())-1))
root.after(1000,countdown)
def ex():
root.after_cancel(countdown)
root.destroy()
def timer():
global root
global master
global frame
root = Tk()
master = root
frame = Frame(master)
global hour_entry
hour_entry = Entry(master,width=3)
hour_entry.grid(row=0,column=0)
colon_l = Label(master,text=':').grid(row=0,column=1)
global minute_entry
minute_entry = Entry(master,width=2)
minute_entry.grid(row=0,column=2)
colon_l2 = Label(master,text=':').grid(row=0,column=3)
global second_entry
second_entry = Entry(master,width=2)
second_entry.grid(row=0,column=4)
global startstop
startstop = Button(master,text="Start",command=timerstartstop)
e = Button(master,text="Exit",command=ex).grid(row=1,column=3)
startstop.grid(row=0,column=5)
root.mainloop()
In addition, I tried to run these two programs from a different starting menu that used the console, which worked.
The console program is:
import timer_prog
timer_prog.timer()
raw_input('next')
import stopwatch
stopwatch.f()
Attached are some screenshots of what the stopwatch program should look like vs what it does look like when called from the starting program.
Note: I can tell the program is running from the starting page, as it prints the current time each second. Also, I attached some screenshots
Stopwatch Program Run Directly
Stopwatch Program Run From The Start Page
Tkinter program should use only one Tk() - to create main window - and one mainloop() - to control all windows and widgets. If you use two Tk() and two mainloop() then it has problem - for example get()/set() may not work.
Subwindows should use Toplevel() instead of Tk().
Function which starts program (ie. run()) could run with parameter window (def run(window)) and then you can execute it as standalone program with
root = Tk()
run(root)
root.mainloop()
or after importing
run(Toplevel())
(without maniloop())
You can use if __name__ == "__main__" to recognize if program starts as standalone.
Example
main.py
from Tkinter import *
class StartPage:
def __init__(self, master):
self.master = master
master.title("Timer Suite: Brian Ton")
Button(master, text="Timer", command=self.run_timer).pack()
Button(master, text="Stopwatch", command=self.run_stopwatch).pack()
def run_stopwatch(self):
import stopwatch
window = Toplevel()
stopwatch.run(window)
def run_timer(self):
import timer_prog
window = Toplevel()
timer_prog.timer(window)
self.master.after_cancel(timer_prog)
def main():
root = Tk()
StartPage(root)
root.mainloop()
main()
stopwatch.py
from Tkinter import *
import datetime
def pre_start():
start_button.config(state='disabled')
stop_button.config(state='normal')
reset_button.config(state='disabled')
start()
def start():
global current_time
# stop_button['state'] can be 'normal' or 'active' so better use ` != 'disabled'`
if reset_button['state'] == 'disabled' and stop_button['state'] != 'disabled':
current_time += 1
time_var.set(str(datetime.timedelta(seconds=current_time)))
print(time_var.get())
master.after(1000, start)
def stop():
start_button.config(state='disabled')
stop_button.config(state='disabled')
reset_button.config(state='normal')
def reset():
global current_time
start_button.config(state='normal')
stop_button.config(state='disabled')
reset_button.config(state='disabled')
current_time = 0
time_var.set(str(datetime.timedelta(seconds=0)))
def run(window):
global master
global current_time, time_var
global start_button, stop_button, reset_button
master = window
current_time = 0
time_var = StringVar()
time_var.set(str(datetime.timedelta(seconds=0)))
time_label = Label(window, textvariable=time_var)
time_label.grid(row=1, column=2)
start_button = Button(master, text='Start', command=pre_start, state='normal')
stop_button = Button(master, text='Stop', command=stop, state='disabled')
reset_button = Button(master, text='Reset', command=reset, state='disabled')
start_button.grid(row=2, column=1)
stop_button.grid(row=2, column=2)
reset_button.grid(row=2, column=3)
if __name__ == '__main__':
# it runs only in standalone program
root = Tk()
run(root)
root.mainloop()

Categories