I have a simple Tkinter gui with about 20 buttons on it. When I click on a button, the script runs for about 5 minutes. During which time, I have to wait until the script stops running to click on the other buttons. Is there a way to setup the window so I can click on other button while the first clicked script is running?
from Tkinter import *
import Tkinter as tk
import time
def function1():
time.sleep(60)
print 'function1'
def function2():
time.sleep(60)
print 'function2'
root = Tk()
w = 450 # width for the Tk root
h = 500# height for the Tk root
frame = Frame(root, width=w,height =h)
button1=Button(frame, text = 'function 1',fg='black',command=function1).grid(row=1,column=1)
button2=Button(frame, text = 'function 2',fg='black',command=function2).grid(row=1,column=2)
frame.pack()
root.mainloop()
I want to be able to click on function2 after while function1 is still running
If you trigger a callback that takes 1 minute to run, you're not returning to the main loop for 1 minute, so the GUI can't respond to anything.
There are two common solutions to this.
The first is to use a background thread:
def function1():
time.sleep(60)
print 'function1'
def function1_background():
t = threading.Thread(target=function1)
t.start()
button1 = Button(frame, text='function 1', fg='black', command=function1_background)
This is simple, but it only works when your code is purely doing background work, not touching any of the tkinter widgets.
The only problem here is that you'd have to def 20 extra functions. You don't want to repeat yourself that much—that's 80 lines of repetitive boilerplate code that gets in the way of seeing the code that matters, and 20 chances to make a stupid bug in copy-pasting that's a pain to track down, and 20 places you have to change if you later decide you want, say, processes instead of threads so the work can parallelize better, or a pool of 4 threads with the background tasks queued up.
You can solve that in a few different ways. See this question for more in-depth explanation, but in short, you get Python to do some of the repetitive work for you.
You can def a single helper function:
def background(func):
t = threading.Thread(target=func)
t.start()
… and then lambda 20 separate function:
button1 = Button(frame, text='function 1', fg='black', command=lambda: background(function1))
Alternatively, you can partially apply the function using partial:
button1 = Button(frame, text='function 1', fg='black', command=functools.partial(background, function1))
Or, if you never want to call the functions except in the background, you can write a decorator and apply it to each function at def time:
def background(func):
#functools.wraps(func)
def wrapper():
t = threading.Thread(target=func)
t.start()
return wrapper
#background
def function1():
time.sleep(60)
print 'function1'
If you can't use threads (e.g., because the background work involves fiddling with your tkinter widgets), the alternative is to restructure your code so that, instead of being one monolithic task that takes 1 minute, it's a bunch of separate tasks that each takes a fraction of a second and schedules the next part:
def function1(count=60):
if count > 0:
time.sleep(0.1)
frame.after(0, function1, count-0.1)
else:
print 'function1'
button1 = Button(frame, text='function 1', fg='black', command=function1)
This always works, if you can find a way to do it. Your real work may not be as easy to divide into 0.1-second chunks as a sleep(60) is.
Related
This seems so simple but I can't figure out what I need to do to remedy. I have a tkinter project and on a button press, a function runs that takes several seconds. I want a "loading..." type message while the function is running so it's obvious it's actually working and not crashed. I figured a label would be easy enough and on the first line of the function, have label1.set('loading') but I suppose because of the way functions work, the label doesn't set until the function is done running--which is not helpful.
I made a second short function
def update_status(message):
label1.set(message)
and for the button in tkinter, used command=lambda:[update_status('loading'),search()] in hopes that the update_status() function would run first, alter the label, and then the second search() function that takes upwards of 30 seconds would run. But I get the same effect.
What's the simplest way finish running the update_status() function--thereby updating my label acting as the "status", and THEN run the time consuming search() function?
I'm not opposed to something more complicated like a loading window or something similar, but just wanted something simple (I have not even googled any type of loading window--I'm mostly hung up on how to get 2 functions to run on a button click in a sequential order).
Hey I do not think you need 2 functions to do what you want. You simply have to update your root so that the label is directly updated.
Here is an example:
import tkinter as tk
import time
def update_status(message1, message2):
var.set(message1)
root.update()
time.sleep(5)
var.set(message2)
if __name__ == '__main__':
root = tk.Tk()
root.title("Wait for function")
var = tk.StringVar()
var.set('Waiting for input')
label1 = tk.Label(root, textvariable=var)
label1.pack()
Button1 = tk.Button(root, text="Wait", command=lambda:update_status('loading', 'done'))
Button1.pack()
root.mainloop()
So when i press a button, it should run a while loop for some time, and while that function is running i wanna press another button to active another function
import threading
from tkinter import *
root = Tk()
def running_function(): #running forever
while True:
pass
def print_something(): #i want to run this while the other function is running
pass
button1 = Button(root, text='PRESS1', command=running_function)
button1.pack()
button2 = Button(root, text='PRESS2', command=print_something) # while "running_function" is active i want to be able to press this button
button2.pack()
root.mainloop()
In general, in tkinter programs, you don’t want to have while loops. In your case you could use the after() method:
def running_function(): #running forever
# contents of function elided
root.after(1, running_function)
I know this is a couple years old but maybe this will be helpful to someone. I get around this problem by using threading. This will run the function on a separate thread and allow the application to continue running dynamically at the same time.
thread1 = threading.Thread(target=running_function, args = (your_arg,))
thread1.start()
I am making a GUI using Tkinter with two main buttons: "Start" and "Stop". Could you, please, advise on how to make the "Stop" button to terminate the already running function called by "Start" button for the following code?
The problem as you may expect is that the entire window including the "Stop" button is stuck/not responsive while "start" function is running.
The "start" function extracts some information from a number of html files which may take pretty while (for 20 huge files it can take around 10 minutes), and I would like for a user to be able to interrupt that process at any moment.
from tkinter import *
import Ostap_process_twitter_file_folder
root = Tk()
def start (event):
Ostap_process_twitter_file_folder.start_extraction()
def stop (event):
# stop "start" function
label1 = Label(root, text = "source folder").grid(row=0)
label2 = Label(root, text = "output folder").grid(row=1)
e_sF = Entry(root)
e_oF = Entry(root)
e_sF.grid(row=0, column=1)
e_oF.grid(row=1, column=1)
startButton = Button(root, text = "start")
startButton.grid(row=2)
startButton.bind("<Button-1>", start)
stopButton = Button(root, text = "stop")
stopButton.grid(row=2, column=1)
stopButton.bind("<Button-1>", stop)
root.mainloop()
I suppose that using threads will be a solution for this issue. Although I've been looking through similar questions here on stackoverflow and various introductory resources on threading in Python (not that much introductory, btw), it is still not clear for me how exactly to implement those suggestions to this particular case.
why do you think using threads would be a solution? ...
you cannot stop a thread/process even from the main process that created/called it. (at least not in a mutliplatform way ... if its just linux thats a different story)
instead you need to modify your Ostap_process_twitter_file_folder.start_extraction() to be something more like
halt_flag = False
def start_extraction(self):
while not Ostap_process_twitter_file_folder.halt_flag:
process_next_file()
then to cancel you just do Ostap_process_twitter_file_folder.halt_flag=True
oh since you clarified i think you just want to run it threaded ... I assumed it was already threaded ...
def start(evt):
th = threading.Thread(target=Ostap_process_twitter_file_folder.start_extraction)
th.start()
return th
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.
This question already has an answer here:
Tkinter locks Python when an icon is loaded and tk.mainloop is in a thread
(1 answer)
Closed 7 months ago.
I am new to GUI programming and I want to write a Python program with tkinter. All I want it to do is run a simple function in the background that can be influenced through the GUI.
The function counts from 0 to infinity until a button is pressed. At least that is what I want it to do. But I have no idea how I can run this function in the background, because the mainloop() of tkinter has control all the time. And if I start the function in an endless loop, the mainloop() cannot be executed and the GUI is dead.
I would like to return control back to the mainloop() after each cycle, but how can I get the control back from the mainloop() to the runapp-function without a user-triggered event?
Here is some sample code that kills the GUI:
from Tkinter import *
class App:
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.button = Button(frame, text="START", command=self.runapp)
self.button.pack(side=LEFT)
self.hi_there = Button(frame, text="RESTART", command=self.restart)
self.hi_there.pack(side=LEFT)
self.runapp()
def restart(self):
print "Now we are restarting..."
def runapp(self):
counter = 0
while (1):
counter =+ 1
time.sleep(0.1)
Event based programming is conceptually simple. Just imagine that at the end of your program file is a simple infinite loop:
while <we have not been told to exit>:
<pull an event off of the queue>
<process the event>
So, all you need to do to run some small task continually is break it down into bite-sized pieces and place those pieces on the event queue. Each time through the loop the next iteration of your calculation will be performed automatically.
You can place objects on the event queue with the after method. So, create a method that increments the number, then reschedules itself to run a few milliseconds later. It would look something like:
def add_one(self):
self.counter += 1
self.after(1000, self.add_one)
The above will update the counter once a second. When your program initializes you call it once, and from then after it causes itself to be called again and again, etc.
This method only works if you can break your large problem (in your case "count forever") into small steps ("add one"). If you are doing something like a slow database query or huge computation this technique won't necessarily work.
You will find the answer in this other question Tkinter locks python when Icon loaded and tk.mainloop in a thread.
In a nutshell, you need to have two threads, one for tkinter and one for the background task.
Try to understand this example : clock updating in backgroud, and updating GUI ( no need for 2 threads ).
# use Tkinter to show a digital clock
# tested with Python24 vegaseat 10sep2006
from Tkinter import *
import time
root = Tk()
time1 = ''
clock = Label(root, font=('times', 20, 'bold'), bg='green')
clock.pack(fill=BOTH, expand=1)
def tick():
global time1
# get the current local time from the PC
time2 = time.strftime('%H:%M:%S')
# if time string has changed, update it
if time2 != time1:
time1 = time2
clock.config(text=time2)
# calls itself every 200 milliseconds
# to update the time display as needed
# could use >200 ms, but display gets jerky
clock.after(200, tick)
tick()
root.mainloop( )
credits: link to site
I don't have sufficient reputation to comment on Bryan Oakley's answer (which I found to be very effective in my program), so I'll add my experience here. I've found that depending on how long your background function takes to run, and how precise you want the time interval to be, it can be better to put self.after call at the beginning of the recurring function. In Bryan's example, that would look like
def add_one(self):
self.after(1000, self.add_one)
self.counter += 1
Doing it this way ensures that the interval of time is respected exactly, negating any interval drift that might occur if your function takes a long time.
If you don't want to be away from those threads, I would like to give one suggestion for your GUI-
Place the function for your GUI just before the root.mainloop() statement.
Example-
root = tk.Tk()
.
.
graphicsfunction() #function for triggering the graphics or any other background
#function
root.mainloop()
Please up vote if you like.