Search code examples
pythonmultithreadingpython-3.xtkinterpython-multithreading

Tkinter: Scheduling Sequential Threads executed via button clicks


I have a Tkinter GUI with multiple buttons each which call different functions using threads. I have to automate the clicking of the buttons in sequence. So I am using a single START button, which will click first button, wait for the respective function's execution to complete and then click the next button, and so on.

I used threads as I needed to keep a Progressbar running while any function is running.

I am also changing the color of the button text from Red (not run yet) to Blue (being run) to Green (finished execution). I know I need to use join() somewhere, but it's not working.

This current code runs all the button invoke() methods at once, and not in a sequence.

import tkinter as tk
from tkinter import ttk

from threading import Thread
def sample_function():
    for i in range(1,10000) :
        print(i)

def run_function(name, func,btn_variable):
    # Disable all buttons
    btn_variable.configure(style = 'blue.TButton')
    processing_bar.start(interval=10)
    print(name, 'started')
    func()
    processing_bar.stop()
    print(name, 'stopped')
    btn_variable.configure(style = 'green.TButton')

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


def prepare_clicked():
    run_thread('prepare', sample_function,prepare_btn)
    prepare_btn.configure(style = 'green.TButton')


def social_clicked():
    run_thread('social', sample_function,social_btn)
    social_btn.configure(style = 'green.TButton')


def anomaly_clicked():
    run_thread('anomaly', sample_function,anomaly_btn)
    anomaly_btn.configure(style = 'green.TButton')

def scoring_clicked():
    run_thread('scoring', sample_function,scoring_btn)
    scoring_btn.configure(style = 'green.TButton')

def dashboard_clicked():
    run_thread('dashboard', sample_function,dashboard_btn)
    dashboard_btn.configure(style = 'green.TButton')


def start_all():
    prepare_btn.invoke()
    anomaly_btn.invoke()
    social_btn.invoke()
    scoring_btn.invoke()
    dashboard_btn.invoke()



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

topFrame = tk.Frame(window)
bottomFrame = tk.Frame(window)

# Tell the Frame to fill the whole window
topFrame.pack(fill=tk.BOTH, expand=1)
bottomFrame.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(5):
    topFrame.rowconfigure(i, weight=1)

bottomFrame.rowconfigure(0, weight=1)
for i in range(3):
    bottomFrame.columnconfigure(i, weight=1)

ttk.Style().configure('blue.TButton', foreground='blue')
ttk.Style().configure('green.TButton', foreground='green')
ttk.Style().configure('red.TButton', foreground='red')


prepare_btn = ttk.Button(topFrame, command=prepare_clicked, text='Button 1',style = 'red.TButton')
anomaly_btn = ttk.Button(topFrame,command=anomaly_clicked, text='Button 2',style = 'red.TButton')
social_btn = ttk.Button(topFrame, command=social_clicked, text='Button 3',style = 'red.TButton')
scoring_btn = ttk.Button(topFrame, command=scoring_clicked, text='Button 4',style = 'red.TButton')
dashboard_btn = ttk.Button(topFrame, command=dashboard_clicked, text='Button 5',style = 'red.TButton')
commentary = ttk.Button(bottomFrame,text='START',width=10,command = start_all)
commentarylabel = ttk.Label(bottomFrame,text=' Commentary ',width=25)
processing_bar = ttk.Progressbar(bottomFrame, orient='horizontal', mode='indeterminate')

buttons = [prepare_btn, anomaly_btn, social_btn,scoring_btn,dashboard_btn]


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')
scoring_btn.grid(row=3, column=0, columnspan=1, sticky='EWNS')
dashboard_btn.grid(row=4, column=0, columnspan=1, sticky='EWNS')
commentary.grid(row=0, column=0, columnspan=1, sticky='EWNS')
commentarylabel.grid(row=0,column = 1, columnspan=2, sticky='EWNS')
processing_bar.grid(row=0, column=3,columnspan=1, sticky='EWNS')

window.mainloop()

Solution

  • Here's something to demonstrates how to do what you want. It works because the code running in threads other than the main one don't make it any tkinter calls. To get the threads to run sequentially one-after-the-other, it uses a FIFO Queue of entries representing each one and starts new ones running when the last one has finished.

    To "schedule" a sequence of steps to be run in a certain order as is done in the start_all() function, all that needs to be done is to put() the information about each one in the order they should be executed in this job_queue.

    This is all accomplished by repeatedly using the universal after() method to periodically run a "polling" function (named poll) which, among other things, checks to see if another thread is currently executing or not and reacts accordingly.

    In the code, the processing each thread does is referred to as either a "step" or a "job".

    from queue import Queue, Empty
    import random
    import tkinter as tk
    from tkinter import ttk
    import tkinter.messagebox as tkMessageBox
    from threading import Thread
    from time import sleep
    
    random.seed(42)  # Generate repeatable sequence for testing.
    
    ITERATIONS = 100
    POLLING_RATE = 100  # millisecs.
    
    # Global variables
    cur_thread = None  # Current running thread.
    cur_button = None
    cur_name = None
    job_queue = Queue()  # FIFO queue.
    
    def sample_function():
        for i in range(1, ITERATIONS):
            print(i)
            sleep(0.01)  # Simulate slow I/O.
    
    def start_thread(name, func, btn_variable):
        global cur_thread, cur_button
    
        if cur_thread is not None:
            tkMessageBox.showerror('Error', "You can't start a step when there"
                                            " are some already running.")
            return
    
        cur_thread = Thread(target=func)
        cur_button = btn_variable
    
        btn_variable.configure(style='blue.TButton')
        cur_thread.start()
    
    def poll(window, processing_bar):
        global cur_thread, cur_button
    
        if cur_thread is not None:
            if cur_thread.is_alive():
                processing_bar.step()
            else:
                cur_thread.join() # Should be immediate.
                cur_thread = None
                processing_bar.stop()
                cur_button.configure(style='green.TButton')
                window.update()
        elif not job_queue.empty():  # More to do?
            try:
                job_info = job_queue.get_nowait()  # Non-blocking.
                start_thread(*job_info)
                processing_bar.start()
                window.update()
            except Empty:  # Just in case (shouldn't happen).
                pass
    
        window.after(POLLING_RATE, poll, window, processing_bar)
    
    # Button commands.
    def prepare_clicked():
        start_thread('prepare', sample_function, prepare_btn)
    
    def social_clicked():
        start_thread('social', sample_function, social_btn)
    
    def anomaly_clicked():
        start_thread('anomaly', sample_function, anomaly_btn)
    
    def scoring_clicked():
        start_thread('scoring', sample_function, scoring_btn)
    
    def dashboard_clicked():
        start_thread('dashboard', sample_function, dashboard_btn)
    
    def start_all():
        global job_queue
    
        # Put info for each step in the job queue to be run.
        for job_info in (('prepare', sample_function, prepare_btn),
                         ('social', sample_function, social_btn),
                         ('anomaly', sample_function, anomaly_btn),
                         ('scoring', sample_function, scoring_btn),
                         ('dashboard', sample_function, dashboard_btn)):
            job_queue.put(job_info)
        # Start the polling.
        window.after(POLLING_RATE, poll, window, processing_bar)
    
    ####
    window = tk.Tk()
    #window = tk.Toplevel()
    
    topFrame = tk.Frame(window)
    bottomFrame = tk.Frame(window)
    
    # Tell the Frame to fill the whole window
    topFrame.pack(fill=tk.BOTH, expand=1)
    bottomFrame.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(5):
        topFrame.rowconfigure(i, weight=1)
    
    bottomFrame.rowconfigure(0, weight=1)
    for i in range(3):
        bottomFrame.columnconfigure(i, weight=1)
    
    ttk.Style().configure('blue.TButton', foreground='blue')
    ttk.Style().configure('green.TButton', foreground='green')
    ttk.Style().configure('red.TButton', foreground='red')
    
    prepare_btn     = ttk.Button(topFrame, command=prepare_clicked, text='Button 1', style='red.TButton')
    anomaly_btn     = ttk.Button(topFrame, command=anomaly_clicked, text='Button 2', style='red.TButton')
    social_btn      = ttk.Button(topFrame, command=social_clicked, text='Button 3', style='red.TButton')
    scoring_btn     = ttk.Button(topFrame, command=scoring_clicked, text='Button 4', style='red.TButton')
    dashboard_btn   = ttk.Button(topFrame, command=dashboard_clicked, text='Button 5', style='red.TButton')
    
    commentary      = ttk.Button(bottomFrame, text='START', width=10, command=start_all)
    commentarylabel = ttk.Label(bottomFrame, text=' Commentary ', width=25)
    processing_bar  = ttk.Progressbar(bottomFrame, 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')
    scoring_btn.grid    (row=3, column=0, columnspan=1, sticky='EWNS')
    dashboard_btn.grid  (row=4, column=0, columnspan=1, sticky='EWNS')
    
    commentary.grid     (row=0, column=0, columnspan=1, sticky='EWNS')
    commentarylabel.grid(row=0, column=1, columnspan=2, sticky='EWNS')
    processing_bar.grid (row=0, column=3,columnspan=1, sticky='EWNS')
    
    window.after(POLLING_RATE, poll, window, processing_bar)
    window.mainloop()