2

I have a GUI which has two buttons and a progressbar stacked on a single column. Each button calls a different function which takes some time to execute. I want the progress bar to move when someone clicks any of the two buttons and keep moving (indeterminately) until the function finishes and then stop. I know I need to use multi-threading but I can't seem to get the code right!

Code

from tkinter import Tk
import time 
from tkinter import *
from tkinter import Button
from tkinter import Frame
from tkinter import ttk
import threading


def sample_function():
    time.sleep(2)  # SAMPLE FUNCTION BEING CALLED

def prepare_clicked():
    sample_function()  

def social_clicked():

    sample_function()

def anomaly_clicked():
    sample_function()             



window = Toplevel() # Tried using Tk but I am using image to design each buttons through the button config in my actual code and tk throws error
topFrame = Frame(window)
topFrame.pack()

prepare_btn = Button(topFrame, command=prepare_clicked,text='Button1')
anomaly_btn = Button(topFrame,command=anomaly_clicked,text='Button2')
social_btn = Button(topFrame, command=social_clicked,text='Button3')
processing_bar = ttk.Progressbar(topFrame, orient='horizontal', mode='indeterminate')



window.rowconfigure((0,1), weight=1)  # make buttons stretch when
window.columnconfigure((0,3), weight=1)  # when window is resized
prepare_btn.grid(row=0, column=1, columnspan=1, sticky='EWNS')
anomaly_btn.grid(row=1, column=1, columnspan=1, sticky='EWNS')
social_btn.grid(row=2, column=1, columnspan=1, sticky='EWNS')
processing_bar.grid(row=3, column=1, columnspan=1, sticky='EWNS')


window.mainloop()
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
ALEX MATHEW
  • 251
  • 1
  • 5
  • 13
  • It's hard to say without seeing some code, preferably a [MCVE] (you can use "dummy" functions that call `time.sleep` to simulate the functions that take time to execute). The main thing with Tkinter and threading is that Tkinter likes to be in the main thread, and may not play well otherwise. – PM 2Ring May 25 '18 at 10:05
  • @PM2Ring I have added a sample code. Please check if you can help. – ALEX MATHEW May 26 '18 at 09:47
  • I'm a little concerned that you're having problems with image buttons. [This answer](https://stackoverflow.com/a/31498692/4014959) may help with that, but if it doesn't then you should ask a fresh question (with its own MCVE). – PM 2Ring May 26 '18 at 12:19

1 Answers1

2

I've added threading to your code. I assume you don't want any of the buttons to be pressable while a function is in progress. If you don't need that, just get rid of the for loops in run_function that change btn['state']. I've also fixed the row & column configuration code so that the widgets expand & contract when the user resizes the window. And I got rid of the evil "star" import.

import tkinter as tk
from tkinter import ttk
import time
from threading import Thread

def sample_function():
    time.sleep(2)

def run_function(name, func):
    # Disable all buttons
    for btn in buttons:
        btn['state'] = 'disabled'

    processing_bar.start(interval=10)
    print(name, 'started')
    func()
    processing_bar.stop()
    print(name, 'stopped')

    # Enable all buttons
    for btn in buttons:
        btn['state'] = 'normal'

def run_thread(name, func):
    Thread(target=run_function, args=(name, func)).start()

def prepare_clicked():
    run_thread('prepare', sample_function)

def social_clicked():
    run_thread('social', sample_function)

def anomaly_clicked():
    run_thread('anomaly', sample_function)


window = tk.Tk()
#window = tk.Toplevel()

topFrame = tk.Frame(window)

# Tell the Frame to fill the whole window
topFrame.pack(fill=tk.BOTH, expand=1)

# Make the Frame grid contents expand & contract with the window
topFrame.columnconfigure(0, weight=1)
for i in range(4):
    topFrame.rowconfigure(i, weight=1)

prepare_btn = tk.Button(topFrame, command=prepare_clicked, text='Button1')
anomaly_btn = tk.Button(topFrame,command=anomaly_clicked, text='Button2')
social_btn = tk.Button(topFrame, command=social_clicked, text='Button3')
buttons = [prepare_btn, anomaly_btn, social_btn]

processing_bar = ttk.Progressbar(topFrame, orient='horizontal', mode='indeterminate')

prepare_btn.grid(row=0, column=0, columnspan=1, sticky='EWNS')
anomaly_btn.grid(row=1, column=0, columnspan=1, sticky='EWNS')
social_btn.grid(row=2, column=0, columnspan=1, sticky='EWNS')
processing_bar.grid(row=3, column=0, columnspan=1, sticky='EWNS')

window.mainloop()

Update

Here's the new improved version, with an 'All' button that runs all the functions, in order. Enjoy!

import tkinter as tk
from tkinter import ttk
import time
from threading import Thread

def prepare_func():
    print('prepare started')
    time.sleep(2)
    print('prepare stopped')

def anomaly_func():
    print('anomaly started')
    time.sleep(2)
    print('anomaly stopped')

def social_func():
    print('social started')
    time.sleep(2)
    print('social stopped')

def all_func():
    print('all started')
    show_and_run(prepare_func, buttons['Prepare'])
    show_and_run(anomaly_func, buttons['Anomaly'])
    show_and_run(social_func, buttons['Social'])
    print('all stopped')

def show_and_run(func, btn):
    # Save current button color and change it to green
    oldcolor = btn['bg']
    btn['bg'] = 'green'

    # Call the function
    func()

    # Restore original button color
    btn['bg'] = oldcolor

def run_function(func, btn):
    # Disable all buttons
    for b in buttons.values():
        b['state'] = 'disabled'

    processing_bar.start(interval=10)
    show_and_run(func, btn)
    processing_bar.stop()

    # Enable all buttons
    for b in buttons.values():
        b['state'] = 'normal'

def clicked(func, btn):
    Thread(target=run_function, args=(func, btn)).start()

window = tk.Tk()
#window = tk.Toplevel()

topFrame = tk.Frame(window)

# Tell the Frame to fill the whole window
topFrame.pack(fill=tk.BOTH, expand=1)

# Make the Frame grid contents expand & contract with the window
topFrame.columnconfigure(0, weight=1)
for i in range(4):
    topFrame.rowconfigure(i, weight=1)

button_data = (
    ('Prepare', prepare_func),
    ('Anomaly', anomaly_func),
    ('Social', social_func),
    ('All', all_func),
)

# Make all the buttons and save them in a dict
buttons = {}
for row, (name, func) in enumerate(button_data):
    btn = tk.Button(topFrame, text=name)
    btn.config(command=lambda f=func, b=btn: clicked(f, b))
    btn.grid(row=row, column=0, columnspan=1, sticky='EWNS')
    buttons[name] = btn
row += 1

processing_bar = ttk.Progressbar(topFrame, 
    orient='horizontal', mode='indeterminate')
processing_bar.grid(row=row, column=0, columnspan=1, sticky='EWNS')

window.mainloop()
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • Thanks. This is what I was looking for !. I have one more request. If I wanted to automatically call a single function using a new button "CALL ALL", which will then invoke all the other functions, but in sequence, ie. wait for first function of previous button to finish before executing the next one, How would I accomplish this ? – ALEX MATHEW May 27 '18 at 11:43
  • Yes. But the respective threads have to be in order of the button sequence, not simultaneously! Thanks – ALEX MATHEW May 27 '18 at 11:48
  • I think I didn't get my request across clearly. My task is to have the 3 buttons I've made, which call some function, and there is a progress bar which keeps moving. Now, I have to automate the clicking of the above three buttons, through a single button. i.e.. The invoking of the above three functions should take place automatically, with the 'clicking of each button' being visible in the GUI. I was going to add color to the button text to show the process change of before,during and after clicks. I need the "ALL'" button to implement an 'automatic demo' of the application itself. – ALEX MATHEW May 27 '18 at 15:00
  • @ALEXMATHEW I've updated my answer with the latest version. – PM 2Ring May 27 '18 at 15:58
  • Thanks! That's all I need. You're awesome ! – ALEX MATHEW May 27 '18 at 18:23