0

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

jason
  • 3,811
  • 18
  • 92
  • 147
  • Could you maybe state your code in your question? It isn't clear _how_ you want your code to run. –  Sep 09 '18 at 01:45
  • As a side note, when you do `button1 = Button(…).grid(…)`, you're just assigning `None` to `button1`, not the `Button` object. The `grid` method doesn't return anything. You need two statements, `button1 = Button(…)`, and then a separate `button1.grid(…)`. – abarnert Sep 09 '18 at 02:25

1 Answers1

5

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.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Yeah you are right. I can't split the other functions into increments. But the first suggestion sounds promising. However since I have about 20 buttons I would have to create 20 of these `_background` functions. Is there a way to create a general function to run all 20 through? Can you show me how to generalize for just these two functions? – jason Sep 09 '18 at 12:02
  • It doesn't seem to work when I pass it through like this `def function1_background(function_name):` then `t = threading.Thread(target=function_name)` – jason Sep 09 '18 at 14:13
  • @jason That will work to define a general backgrounding function, but then you have to set the `command` right. I’ll edit to show you how to do that. – abarnert Sep 09 '18 at 19:51
  • This is incredibly thorough. Thanks – Chris Conlan Sep 09 '18 at 20:11
  • @abarnert. I see my mistake. I was passing it as `background(lambda: ...)` so it was initiating before I pressed the button. – jason Sep 09 '18 at 23:34
  • now I have a double lambda `lambda: background( lambda: ...)` and that seems to work. – jason Sep 10 '18 at 00:10
  • @jason You probably don't need the inner lambda, but without seeing your actual code, it's hard to say for sure. – abarnert Sep 10 '18 at 00:11