I have a python file that contains:
from tkinter import *
from tkinter.scrolledtext import ScrolledText
import time
root = Tk()
scroll_text = ScrolledText(root)
scroll_text.pack()
def task0():
time.sleep(5) # some code which takes 5 secs
pass
def task1():
# some code which takes a few seconds
pass
def task2():
# some code which takes a few seconds
pass
# ...
for i in range(0, 5):
scroll_text.insert(INSERT, 'program start to do task{0}\n'.format(i))
if i == 0:
task0()
elif i == 1:
task1()
elif i == 2:
task1()
# ...
print('Finish')
root.mainloop()
I expect when program starts immediately see the program start to do task .. text in the tkinter window and after that start executing functions, each lasting a few seconds.
What should I do to make the events occur in the same order as the code I wrote (ie, first show the task number with program start to do task .. to show the user the progress of the program in the tkinter window and then execute functions).
TKinter is not updating the window because the mainloop() is blocked from running by your loop. The solution depends on what actual problem you are trying to solve, which isn't clear from a blank loop. If you're simply trying to create a 5-second delay, use tkinter's after() method to schedule your print() command to run later without blocking the mainloop() until then.
Your window isn't created until root.mainloop() is executed. Your programs first waits then creates the window.
What you can do is, add this line:
root.after(5000,lambda: print('Finish'))
After 5 second(5000 milliseconds), it will call the lambda function, which will print.
Related
I created an example code because my original is too big and has private information(My own) in it.
While running a program from a Tkinter GUI, it runs the program but makes the GUI unresponsive because of time.sleep() blocking the GUI from updating.
I am trying to avoid using timers because it fires a different function after a duration instead of simply pausing the function and then continuing the same function.
Is there an alternative that does not block the GUI but still adds a delay inside of the function?
Example Code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
time.sleep(1) **# - here blocks everything outside of the function**
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value = 1
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
Edit: Thanks so much, you're answers were fast and helpful, I changed the code and added "wn.mainloop()" after the delay and replaced "time.sleep(1)" with wn.after(100, wn.after(10, MyLabel.config(text=Value))
here is the final code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 0
while Value < 10:
print("Do something")
wn.after(10, MyLabel.config(text=Value))
Value += 1
wn.mainloop()
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
The short answer is that you can use wn.after() to request a callback after a certain amount of time. That's how you handle it. You get a timer tick at a one-per-second rate, and you have enough state information to let you proceed to the next state, then you go back to the main loop.
Put another way, timers are exactly how you have to solve this problem.
Fundamentally, any callback function in Tkinter runs in the main GUI thread, and so the GUI thread will block until the function exits. Thus you cannot add a delay inside the function without causing the GUI thread to be delayed.
There are two ways to solve this. One would be to refactor your function into multiple pieces so that it can schedule the remaining work (in a separate function) via .after. This has the advantage of ensuring that all of your functions are running in the main thread, so you can perform GUI operations directly.
The other way is to run your function in a separate thread that is kicked off whenever your main callback is executed. This lets you keep all the logic inside the one function, but it can no longer perform GUI operations directly - instead, any GUI operations would have to go through an event queue that you manage from the main thread.
You can combine after() and wait_variable() to simulate time.sleep() without blocking tkinter from handling pending events and updates:
def tk_sleep(delay):
v = wn.IntVar()
# update variable "delay" ms later
wn.after(delay, v.set, 0)
# wait for update of variable
wn.wait_variable(v)
Using tk_sleep() in your while loop:
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
tk_sleep(1000) # waits for one second
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value += 1
Please read my question carefully - I know there are plenty of ways to implement a countdown timer on Tkinter without freezing the window, but all of the existing solutions also cause the code to be non-blocking. For my use case, I need to schedule a task to run automatically after time's up while keeping the GUI active (not frozen). My guess is that I need to somehow block the execution of the next task, but that will also freeze the GUI window. So is there any way out?
What I have so far:
root = Tk.Tk()
def countdown(time, msg='Counting down'):
def tick():
nonlocal time
status(f'{msg} ({60 - time}sec)')
time += 1
root.after(1000, tick)
where status() is simply a function that updates the text of some buttons.
The current count down function does not work by itself as I don't have a way to stop the after() after the timeout period.
The other parts of the program will be like:
countdown(10) # I need this line to be blocking or somehow prevents the code from going to next line
print('starting scheduled job...')
job()
I have tried to use threading but as I said earlier on, this causes the code to be non-blocking, and the moment I use Thread.join(), the entire GUI freezes again.
Currently, your question doesn't make a lot of sense to me. From what I understand you want your job() function to be called after the countdown.
Using thread for this is unnecessary. You can just use after and once the timer reaches 0 call the job() function.
Here is a minimal example
import tkinter as tk
def job():
status.config(text="starting job")
def countdown(time, msg='Counting down'):
time -= 1
status.config(text=f'{msg} ({time}sec)')
if time != 0:
root.after(1000, countdown, time)
else:
job() # if job is blocking then create a thread
root = tk.Tk()
status = tk.Label(root)
status.pack()
countdown(20)
root.mainloop()
Note: I may have overthought this and the other answer may actually be simpler perhaps
From my understanding you want to create a program that will after a certain time run another task, but neither the task, nor countdown should interfere with the GUI (but the task must run only after countdown), explanation in code comments:
# import what is needed
from tkinter import Tk, Button, Label
from threading import Thread
from queue import Queue, Empty
import time
# the function that the button will call to start the countdown
# and after that the task
def start_countdown():
# disable button so not to accidentally run the task again
# before it has even started
button.config(state='disabled')
# create a queue object
queue = Queue()
update_label(queue)
# set daemon=True to kill thread if the main thread exits
Thread(target=countdown, args=(queue, ), daemon=True).start()
# the task you want to do after countdown
def do_task():
for _ in range(10):
print('doing task...')
time.sleep(0.5)
# the actual countdown (btw using `time.sleep()` is more precise
# and only the thread will sleep)
# put data in queue so that it can easily be accessed
# from the main thread
def countdown(queue):
seconds = 10
for i in range(1, seconds + 1):
queue.put(f'Seconds left: {seconds + 1 - i}')
time.sleep(1)
queue.put('Starting task')
# place a sentinel to tell the reading part
# that it can stop
queue.put('done')
# do the task, this will run it in the same thread
# so it won't block the main thread
do_task()
# function to update the label that shows the users how many seconds left
def update_label(queue):
try:
# since block=False it will raise an exception
# if nothing is in queue
data = queue.get(block=False)
except Empty: # therefore except it and simply pass
pass
else:
# if no error was raised check if data is sentinel,
# if it is, stop this loop and enable the button (if needed)
if data == 'done':
button.config(state='normal')
return
# otherwise just update the label with data in the queue
label.config(text=data)
finally:
# and (almost) no matter what happens (nothing much should) loop this again
root.after(100, update_label, queue)
# basic tkinter setup
root = Tk()
root.geometry('300x200')
button = Button(root, text='Start countdown', command=start_countdown)
button.pack(expand=True)
label = Label(root)
label.pack(expand=True)
root.mainloop()
I would like to understand why this code:
import time
for i in range(1,11):
print(i)
time.sleep(1)
shows (as it should!) numbers from 1 to 10, each every 1 second, while this code:
from tkinter import *
import time
root = Tk()
for i in range(1,11):
Label(root, text = i).grid(row=0, column=i-1, padx=5, pady =5)
time.sleep(1)
root.mainloop()
waits for 10 seconds, and then displays a window with the 10 numbers (instead of adding them one by one).
I am aware this is a silly question, but I really can't understand! Many Thanks! Alessandro
Most GUI's work differently to what you expect.
They work in an asynchronous way, which means, that you setup your windows and start an event loop.
This event loop will display all widgets, labels, etc, that you set up before calling the event loop and wait for any events (GUI events like mouse or keyboard events, timer events and perhaps network events).
When any event is encountered code associated to that event will be called and this code can request to change the GUI (show or hide elements, change labels or attributes of graphical widgets) However the change to the GUI will only be performed when you give control back to the event loop (when the code handling an event finished)
In your given code you change a label in a for loop with sleep statements, but only after the for loop is finished your main loop is being called and this is the moment, where the final state of your GUI will be displayed.
So what you encounter is a know issue for almost all GUI / asynhronous kind of applications.
You have to rewrite your code such, that you start a timer event, and when the timer event fires a function will set a label and increase the counter by 1. And if the counter is not 11 it will restart another timer
This is because the time.sleep function is before the root.mainloop function.
root.mainloop is what causes the window to appear on-screen and start doing things. Instead, I'd recommend using window.after, as that tells the window to run a function after some time when it's on-screen.
Here's an example of a modification you could make (it's not that good but it works):
from tkinter import *
import time
root = Tk()
progress = 0
end = 10
def update_progress():
global progress
progress += 1
Label(root, text = progress).grid(row=0, column=progress-1, padx=5, pady =5)
if progress < end: root.after(1000,update_progress) # Tell the window to call this function in 1000ms (1 second)
root.after(0,update_progress) # Tell the window to run the update_progress function 0ms after now.
root.mainloop()
I'd recommend looking at gelonida's answer for an explanation of why your original code didn't work, and what you need to keep in mind when programming with GUIs in the future.
I was racking my brain a bit trying to figure out why a tkinter window would only appear after I had stopped my script. Turns out, it won't appear if the delay time in my root.after (that is within my infinite fruity loop) was set to 0. Setting it to 1 or higher caused it to work correctly. Is this a bug or am I missing something important about how .after works? I'm running this with Python 2.7 in Anaconda on mac OS.
import time
import Tkinter as tk
import random
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
frame = tk.Frame(root, width=210, height=210)
frame.pack()
luckynumber = tk.IntVar()
label1 = tk.Label(frame, text="random number").pack(side=tk.LEFT)
display1 = tk.Label(frame, textvariable=luckynumber)
display1.pack( side=tk.LEFT )
def askrandy():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
def fruityloop():
time.sleep(.5)
askrandy()
root.after(1, fruityloop)
root.after(0, fruityloop)
root.mainloop()
Second question: this code doesn't run very smoothly. Seeing as it's quite simple, I assumed it would be pretty solid. But I find that it takes a couple seconds to get started and moving the window around causes it to stutter as well. Would this work better with my main loop run as a class?
This is normal behavior.
Tkinter maintains a queue of work to be done when it goes idle. This is the "idle" queue.
When you call after, the function you supply is added to this queue. When the main event loop (or a call to after_idle) processes the queue, it looks for items on the queue that should be run based on the current time and the time that the item should be run. All items that are due to be run are run before processing of the queue stops.
If one of those adds an item to the queue with a value of zero it will be run since its time is due. If that item itself adds an item to the queue, then you take one item off of the queue and immediately put one one so the queue will never become empty. If the queue never becomes empty, tkinter isn't able to process other types of events.
The reason that the program seems slow and jerky is because of the call to sleep. When you call sleep, tkinter does exactly that: it sleeps. It cannot process any events, even events that simply refresh the window. If you want askrandy to be called once every half second, you should simply call after with a value of 500, rather than call it with a value of zero and then sleep for half a second.
Whether the main window is a class or not will not affect your program all all. You simply need to stop using sleep, and provide sane values to after. If you are trying to show a simple animation, a value of 30 is about as small as you need to go.
This is how it should looks without sleep(). But I don't know if it can help. It works fast on Linux.
If you run code in IDLE then you may have problem because it uses Tkinter to display windows and runs own mainloop() but Tkinter should run only one mainloop(). You can try directly in console python script.py.
import Tkinter as tk
import random
# --- functions ---
def fruityloop():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
# run again after 500ms = 0.5s
root.after(500, fruityloop)
# --- main ---
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
luckynumber = tk.IntVar()
frame = tk.Frame(root, width=210, height=210)
frame.pack()
label = tk.Label(frame, text="random number")
label.pack(side=tk.LEFT)
display = tk.Label(frame, textvariable=luckynumber)
display.pack(side=tk.LEFT)
# run first time
fruityloop()
root.mainloop()
I've this simple script and I would love to understand why it's ignoring Ctrl-C.
from Tkinter import *
def main():
root = Tk().withdraw()
mainloop()
if __name__ == "__main__":
main()
I've found this other question on SO (tkinter keyboard interrupt isn't handled until tkinter frame is raised) which is basically the same. Unfortunately, the answer is not clear to me. How should I use the after() function in order to catch the signal?
Moreover, I need Tkinter only to use tkSnack and I don't want any windows to appear. So, adding a button to call root.quit() it's not a possibility.
This seems to work for me if I shift focus back to the command window before typing ctrl-C.
from Tkinter import *
def dummy(root):
root.after(1000, dummy, root)
print '',
def main():
root = Tk()
root.withdraw()
dummy(root)
mainloop()
if __name__ == "__main__":
main()
You can drive this as a Tk application with a hidden window. Following up on your previous question as well where this is all part of an mp3 player here is a sample.
from Tkinter import *
import sys,tkSnack
def terminate(root):
root.quit()
def main(args = None):
if args is None:
args = sys.argv
root = Tk()
root.withdraw()
tkSnack.initializeSnack(root)
root.after(5000, terminate, root)
track = tkSnack.Sound(file=args[1])
track.play()
root.mainloop()
root.destroy()
return 0
if __name__=='__main__':
sys.exit(main())
In this case we hold onto the root Tk window but withdraw it before it gets a chance to be mapped on screen. We then schedule an event for 5 seconds time to terminate the application which will exit the Tk event loop and we can destroy everything and exit cleanly. This works because the play() command causes the sound track to begin playing and returns immediately. The sound data is handled with the event loop - so not running the event loop will halt its playing.
Without the after event causing early termination the application would eventually use up all the sound data from the file and then hang. The play() method takes a command to be run when it finishes playing so we can have the mainloop exit properly at the right time rather than guess the time and use after events.