Home > Net >  Tkinter - Thread communication and shared result - with progressbar
Tkinter - Thread communication and shared result - with progressbar

Time:01-13

I am building an interface in Tkinter in which the main window (let's call it 'root') contains a button (say, 'create'). Furthermore, assume I have already defined a function 'f'. I would like to create the following effect: clicking on 'create' would execute 'f' in the background and at the same time open an indeterminate progress bar in a new window. Moreover, and this is the tricky part for me, I want the progress bar to close automatically after 'f' is done executing. How can I achieve this? Could you please provide a minimal working example? I think that key lies on constructing a proper function to pass as 'command' option to 'create'.

This is what I have thus far. It is not even running properly, as the progress bar runs indefinitely and the task starts being executed only after the progress bar is closed (or after closing 'root'). However, it feels like this is really close, and there is some small issue that I should fix but that I cannot see:

from tkinter import *
from tkinter.ttk import *
import threading
import time

root = Tk() # Main window

def create_command():

    stop_flag = threading.Event()  # create a flag to stop the progress bar

    def f():
        # function to do some task
        print("Starting task...")
        time.sleep(5)  # simulate some time-consuming task
        print("Task complete.")
        stop_flag.set()  # set the stop flag to indicate that progress_check() should stop

    progress_bar_window = Toplevel(root)  # Progress bar window

    progress_bar = Progressbar(progress_bar_window, orient= 'horizontal', length= 300, mode= 'indeterminate') # Create progress bar
    progress_bar.pack()

    progress_bar.start()  

    def progress_check():         
        
        # function to run an infinite loop
        while not stop_flag.is_set():
            
            print("Running infinite loop...")
            time.sleep(1) 
        progress_bar.stop()
        progress_bar_window.destroy() 

    progress_bar_window.mainloop() # Start mainloop for progress bar window
    
    # create separate threads to run the functions
    thread1 = threading.Thread(target=f, args=())
    thread2 = threading.Thread(target=progress_check, args=())
    
    thread1.start()  # start executing f    
    thread2.start()  # start the progress_check   
     

    # wait for f to finish before stopping the infinite loop
    thread2.join()
    stop_flag.set()  # set the stop flag to indicate that progress_bar() should stop

create_button = Button(root, text= "Create", command= create_command)
create_button.pack()

root.mainloop()

CodePudding user response:

Look at this:

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

root = tk.Tk() # Main window

def create_command():
    # create a flag to stop the progress bar
    stop_flag = threading.Event()

    def f():
        print("Starting task...\n", end="")
        time.sleep(5)
        print("Task complete.\n", end="")
        # set the stop flag to indicate that progress_check() should stop
        stop_flag.set()

    progress_bar_window = tk.Toplevel(root)

    progress_bar = ttk.Progressbar(progress_bar_window, orient="horizontal",
                                   length=300, mode="indeterminate")
    progress_bar.pack()
    progress_bar.start()

    def progress_check():
        # If the flag is set (function f has completed):
        if stop_flag.is_set():
            # Stop the progressbar and destroy the toplevel
            progress_bar.stop()
            progress_bar_window.destroy()
        else:
            # If the function is still running:
            print("Running infinite loop...\n", end="")
            # Schedule another call to progress_check in 100 milliseconds
            progress_bar.after(100, progress_check)

    # start executing f in another thread
    threading.Thread(target=f, daemon=True).start()
    # Start the tkinter loop
    progress_check()

create_button = tk.Button(root, text= "Create", command=create_command)
create_button.pack()

root.mainloop()

Explanation:

To run a loop alongside tkinter, you should use .after, like in this question. I changed progress_check so that tkinter calls it every 100 milliseconds until stop_flag is set. When stop_flag is set, the progressbar stops and the Toplevel is destroyed.


A few minor points:

  • from ... import * is discouraged
  • With tkinter, you don't need more than 1 .mainloop() unless you are using .quit(). .mainloop() doesn't stop until all tk.Tk windows have been destroyed.
  • There is no point in creating a new thread, if you are going to call .join() right after.

CodePudding user response:

First of all, don't use wildcard imports!
Wildcard-imports can lead to name conflicts, for instance swap the wildcard imports from ttk and tkinter. You end up using tkinter buttons even if you want to use ttk buttons. Same issue might appear with PhotoImage and pillow. The magic word is "qualified-names".

Also I like to have some sort of structure in my code, I prefer classes. However, even in a procedural code there can be some sort of structure. For instance:

  1. imports
    1.0) built-in modules
    1.1) import external modules
    1.2) import own modules
  2. Constants and global variables
  3. free functions
  4. main window definitions
    ...

every logical block can be separated with comments that indicates what the following code might do or represents. This could also be useful to "jump" with the search function of your IDE to the point you want to work next, in larger scripts and modules this becomes handy.


A slightly different version of your code can be found below and it is not intended to be used:

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

def start_worker_thread():
    'This function starts a thread and pops up a progressbar'
    def generate_waiting_window():
        'nested function to generate progressbar'
        #disable button to inform user of intended use
        start_btn.configure(state=tk.DISABLED)
        #toplevel definitions
        toplevel = tk.Toplevel(root)
        toplevel.focus()
        #progressbar definitions
        progress = ttk.Progressbar(
            toplevel, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
        progress.pack(fill=tk.BOTH, expand=True)
        progress.start()
        return toplevel

    def long_blocking_function():
        'This function simulates a long blocking call'
        stopped = threading.Event()
        n = 0
        while not stopped.is_set():
            n  = 1
            print('working in turn', n)
            time.sleep(0.5)
            if n == 10:
                stopped.set()        
        nonlocal thread_info
        thread_info = n
        #important!! last logical line
        toplevel.destroy()
        return None
    
    toplevel = generate_waiting_window()
    thread_info = None
    thread = threading.Thread(target=long_blocking_function)
    thread.start()
    toplevel.wait_window()
    start_btn.configure(state='normal')
    result_lbl.configure(text='Result is: ' str(thread_info))
    print('thread exited on turn', thread_info)

#Main window definitions
root = tk.Tk()
start_btn = ttk.Button(root, text="Start", command=start_worker_thread)
start_btn.pack()
result_lbl = tk.Label(root, text='Result is: None')
result_lbl.pack()
#start the application
root.mainloop()
#after application is destroyed

While this code is efficient for this simple task, it requires understanding what it does to debug it. That is why you won't find code like this often. It is here for demonstrative purposes. So what is wrong with the code and how does it differ from the meanwhile canonical way of using threads in tkinter.


First of all, it uses nested function. While this might not an issue here, computing the same function over and over again, can slow down your code significantly.
Second it uses tkwait and therefore has some caveats over the linked answer.
Also threading.Event is a low-level primitive for communication, while there are cases you could use it, tkinter offers own tools for it and these should be preferred.
In addition it does not use a threadsafe storage for the data and this could also lead to confusion and non reliable data.


A better approach and a slight improvement to the canonical way can be found here:

import tkinter as tk
from tkinter import ttk
import threading
import sys
import queue
import time

inter_thread_storage = queue.Queue()
temporary_toplevel = None
EXIT = False

def on_thread_ended_event(event):
    start_btn.configure(state=tk.NORMAL)
    result = inter_thread_storage.get_nowait()
    result_lbl.configure(text='Result is: ' str(result))
    global temporary_toplevel
    temporary_toplevel.destroy()
    temporary_toplevel = None

def worker_thread_function():
    'Simulates a long blocking function'
    n = 0
    while n < 10 and not EXIT:
        n  = 1
        print('working in turn', n)
        time.sleep(0.5)
    if not EXIT:
        inter_thread_storage.put(n)
        root.event_generate('<<ThreadEnded>>')

def start_worker_thread():
    'This function starts a thread and pops up a progressbar'
    #toplevel definitions
    toplevel = tk.Toplevel(root)
    toplevel.focus()
    #progressbar definitions
    progress = ttk.Progressbar(
        toplevel, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
    progress.pack(fill=tk.BOTH, expand=True)
    progress.start()
    #thread definitions
    thread = threading.Thread(target=worker_thread_function)
    thread.start()
    #disable button to inform user of intended use
    start_btn.configure(state=tk.DISABLED)
    #store toplevel temporary
    global temporary_toplevel
    temporary_toplevel = toplevel

#Main window definitions
root = tk.Tk()
root.bind('<Destroy>',lambda e:setattr(sys.modules[__name__], 'EXIT', True))
root.bind('<<ThreadEnded>>', on_thread_ended_event)
start_btn = ttk.Button(root, text="Start", command=start_worker_thread)
start_btn.pack()
result_lbl = tk.Label(root, text='Result is: None')
result_lbl.pack()
#start the application
root.mainloop()
#after application is destroyed

This is how it works:

  • generate a new event
  • Make sure your toplevel can be reached, with global or alternatives.
  • store data threadsafe like in a Queue
  • fire the event and let tkinter call your function safely in the mainloop.
  • it has a flag for the edge case, where the user closes the main window before the thread finished.

Let me know, if you have questions to my answer.

  • Related