I'm writing a script that takes some time to complete, but want to give some feedback to the user showing that the program is actually running. I'm trying to use the progress bar from tkinter because it's simple, but when the window closes, the program continues running in the background.
import sys
from time import sleep
from threading import Thread
import tkinter as tk
import tkinter.ttk as ttk
def work(win):
sleep(10)
win.destroy()
root = tk.Tk()
p = ttk.Progressbar(root, mode="indeterminate")
p.pack()
t = Thread(target=lambda: work(root))
t.start() # start thread
p.start() # start progressbar
root.mainloop()
sys.exit()
Here's essentially what the code does. I've tried putting sys.exit() after the thread closes the window, but that yields the same result.
After some debugging it seems like the program keeps running the main loop of root even though I destroyed it, so how can I make sure the program terminates the script properly?
Related
Using tkinter, can I bind an ongoing event to activate while the mainloop still goes off?
Means it executes the same command over and over until the program is closed
you can use root.after to run a function repeatedly like so:
def repeat_task():
# do stuff
root.after(10, repeat_task)
this will constantly do something, and then run itself. the delay time shouldn't be 0 because it may not let tkinter's event loop process other events and will freeze. it will go until the window is closed.
You can also use threading approach:
import tkinter as tk
import threading
def myLoop():
print("Hey I am looping!")
threading.Timer(1, myLoop).start()
window = tk.Tk()
button = tk.Button(window, text = 'Threading', command = lambda: threading.Timer(1, myLoop).start())
button.pack()
window.mainloop()
threading.Timer is an object that will run parallel to your GUI so it won't freeze it.
threading.Timer will fire up every Nth second that you can specify in the constructor.
I am currently coding a program that will do something (e.g count numbers constantly) until something is inputted into a dialog box displayed.
However, whenever I try this, the program freezes when waiting for an input and so does not make any progress in the counting process I am trying to run in the background.
Is there any way to have a timer that continuously runs in the background so that in say 5 minutes, the counter instantly stops and the dialog box disappears? This is a basic skeleton of my code. I used the tkinter dialog box for input and tried to create a timer that will run in the background.
from time import *
from tkinter import *
from tkinter import messagebox
from tkinter import simpledialog
while timer<300:
sleep(1)
timer += 1
ROOT = Tk()
ROOT.withdraw()
USER_INP = simpledialog.askstring(title="Code Required",
prompt="What's the Code?:")
Preferably without external modules but if not that is fine. Thanks in advance :)
This is the code requested
from tkinter import *
from tkinter import simpledialog
root = Tk()
root.withdraw()
def ask():
simpledialog.askstring(title="Code Required",
prompt="What's the Code?:")
## root.after(5000, root.destroy()) #added in the root.after() to try and terminate it after set time
root.after(3000,ask) #triggers ask() after 3000 ms(3 seconds)
root.after(100000, root.destroy()) # tried to wait 10 seconds before it breaks but this doesn't show the dialog box any more
root.mainloop()
Here is a basic code with tkinter that makes the dialogbox pop up after 5 seconds.
from tkinter import *
from tkinter import simpledialog
root = Tk()
root.withdraw()
def ask():
simpledialog.askstring(title="Code Required",
prompt="What's the Code?:")
root.after(5000, root.destroy) #added in the root.after() to try and terminate it after set time
root.after(3000,ask) #triggers ask() after 3000 ms(3 seconds)
#root.after(10000, root.destroy) # tried to wait 10 seconds before it breaks but this doesn't show the dialog box any more
root.mainloop()
Here after() triggers a function after the given time, i.e, 3000 ms(3 sec), so you can adjust the timer, out there too. This is just an example and you can edit this more as you like.
Why use after() and not while and a timer?
This is because a while loop interferes a tkinter mainloop() causing the window to be unresponsive, so it is not recommended to use while or time.sleep(). Instead you could use the built-in after() method by tkinter or threading too.
Here is a bit more on after():
It takes two positional arguments,mainly, ms and func
ms - It is the time(in milliseconds) after which the specified function will be triggered.
func - It is the function to be triggered after the specified ms finises.
WARNING:
Keep in mind that the root window is not destroyed, its just hidden, so as long as the root window is not destroyed, the program keeps on running in the background, so you will have to bring back the window and close it for the task to end. For this reason, ive added root.destroy() there.
Take a look here for a bit more understanding on after()
Hope it cleared your doubts, do let me know if any errors.
Cheers
I know this question has been asked numerous times before, but they all use people's code and classes/various functions, etc, so its been a bit difficult to try and follow/understand what's going on. I wanted a more simpler setup to understand what's happening here and how it all works.
When you thread a function, once the function is complete, the thread is closed:
import threading
def fun():
x=0
while x<1000:
x+=1
threading.Thread(target=fun).start()
So I decided to take this idea one step further with Tkinter.
import tkinter as tk
from tkinter import *
from tkinter import messagebox as mb
import threading
def fun():
x=0
while x<10000900:
x+=1
if x == 50:
print(x)
def main():
while True:
fun()
if mb.askquestion('Replay', 'Would you like to play another round?') != 'yes':
root.destroy()
break
root = tk.Tk()
root.geometry('600x600')
threading.Thread(target=main).start()
root.mainloop()
The idea being, that when the main function was broken (based on the user response), the thread would also close automatically. Additionally, so would the Tkinter window. However, when the above script is run, the Tkinter window does close, but the terminal still indicates something, which I assume is the thread. What I don't understand is why in the first case where I use threading, the program ends properly, whereas the 2nd one doesn't.
When you execute root.destroy() you kill the main thread (mainloop) as well as the extra thread running your main funtion. That way the break statement never gets executed - the thread that would execute that statement is abruptly ended.
If you replace root.destroy() with root.after(10, root.destroy) the program acts as expected. This is because you're delaying the call to root.destroy() by some time (10 ms). This delay allows the break statement to be executed since the thread is still alive for 10 ms.
Part of my code is as follows:
def get_songs():
label6.configure(text='Wait')
os.system('/home/norman/my-startups/grabsongs')
label6.configure(text='Done')
The label is not updated at the first .configure() but is at the second one.
Except if I cause a deliberate error immediately after the first one at which point it is updated and then the program terminates.
The system call takes about 2 minutes to complete so it isn't as if there isn't time to display the first one.
I am using Python 2.7.6
Does anyone know why please?
I'm going to guess you're using Tkinter. If so, as #albert just suggested, you'll want to call label.update_idletasks() or label.update() to tell Tkinter to refresh the display.
As a very crude example to reproduce your problem, let's make a program that will:
Wait 1 second
Do something (sleep for 2 seconds) and update the text to "wait"
Display "done" afterwards
For example:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
Notice that "Wait" will never be displayed.
To fix that, let's call update_idletasks() after initially setting the text:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
label.update_idletasks()
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
As far as why this happens, it actually is because Tkinter doesn't have time to update the label.
Calling configure doesn't automatically force a refresh of the display, it just queues one the next time things are idle. Because you immediately call something that will halt execution of the mainloop (calling an executable and forcing python to halt until it finishes), Tkinter never gets a chance to process the changes to the label.
Notice that while the gui displays "Wait" (while your process/sleep is running) it won't respond to resizing, etc. Python has halted execution until the other process finishes running.
To get around this, consider using subprocess.Popen (or something similar) instead of os.system. You'll then need to perodically poll the returned pipe to see if the subprocess has finished.
As an example (I'm also moving this into a class to keep the scoping from getting excessively confusing):
import Tkinter as tk
import subprocess
class Application(object):
def __init__(self, parent):
self.parent = parent
self.label = tk.Label(parent, text='Not waiting yet')
self.label.pack()
self.parent.after(1000, self.do_stuff)
def do_stuff(self):
self.label.configure(text='Wait')
self._pipe = subprocess.Popen(['/bin/sleep', '2'])
self.poll()
def poll(self):
if self._pipe.poll() is None:
self.label.after(100, self.poll)
else:
self.label.configure(text='Done')
root = tk.Tk()
app = Application(root)
tk.mainloop()
The key difference here is that we can resize/move/interact with the window while we're waiting for the external process to finish. Also note that we never needed to call update_idletasks/update, as Tkinter now does have idle time to update the display.
I was using the following code to examine if Tkinter works along with multithreading. But the following code doesn't work (the Gui becomes unresponsive as soon as it runs). Can anyone please explain why it doesn't work?
from threading import Thread
import tkinter as tk
window = tk.Tk()
label = tk.Label(window, text='Hello')
label.pack()
def func():
i = 1
while True:
label['text'] = str(i)
i += 1
Thread(target=func).start()
Thread(target=window.mainloop).start()
It doesn't work because Tkinter doesn't support multithreading. Everything that interacts with a Tkinter widget needs to run in the main thread. If you want to use multithreading, put the GUI in the main thread and your other code in a worker thread, and communicate between them with a thread safe queue.