Tkinter GUI with progress bar

Question:

I have a simple Tk GUI and a long process in a function attached to a button. I want a progress bar when I click on the button, just like it starts a long process.

How can I do that? This is my current code:

from tkinter import Button, Tk, HORIZONTAL

from tkinter.ttk import Progressbar
import time


class MonApp(Tk):
    def __init__(self):
        super().__init__()

        bt1 = Button(self, text='Traitement', command=self.traitement)
        bt1.grid()
        self.progress = Progressbar(self, orient=HORIZONTAL, length=100, mode='indeterminate')
        self.progress.grid()
        self.progress.grid_forget()

    def traitement(self):
        self.progress.grid()
        self.progress.start()
        time.sleep(15) 
        ## Just like you have many, many code lines...

        self.progress.stop()


if __name__ == '__main__':
    app = MonApp()
    app.mainloop()

How can I put a progress bar in that application?

Asked By: j666

||

Answers:

You can find ttk.Progressbar at tkdocs

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

tk = Tk()
progress = Progressbar(tk, orient=HORIZONTAL, length=100, mode='determinate')


def bar():
    progress['value'] = 20
    tk.update_idletasks()
    time.sleep(1)
    progress['value'] = 50
    tk.update_idletasks()
    time.sleep(1)
    progress['value'] = 80
    tk.update_idletasks()
    time.sleep(1)
    progress['value'] = 100

progress.pack()
Button(tk, text='foo', command=bar).pack()
mainloop()

It’s better to use threading and run your code in another thread.

Like this:

import threading
import time
from tkinter import Button, Tk, HORIZONTAL
from tkinter.ttk import Progressbar

class MonApp(Tk):
    def __init__(self):
        super().__init__()

        self.btn = Button(self, text='Traitement', command=self.traitement)
        self.btn.grid(row=0, column=0)
        self.progress = Progressbar(self, orient=HORIZONTAL, length=100, mode='indeterminate')

    def traitement(self):
        def real_traitement():
            self.progress.grid(row=1,column=0)
            self.progress.start()
            time.sleep(5)
            self.progress.stop()
            self.progress.grid_forget()

            self.btn['state']='normal'

        self.btn['state']='disabled'
        threading.Thread(target=real_traitement).start()


if __name__ == '__main__':
    app = MonApp()
    app.mainloop()
Answered By: xmcp

For all the GUI elements to modify themselves (in your case, for the progress bar to move) the execution must hit app.mainloop().

In your case, def traitement(self): starts and then stops the progressbar without hitting the mainloop, so it fails to visibly reflect the intended progressbar movement on the GUI. The catch here is, when the execution hits mainloop, progressbar is configured to ‘stop’ state.

Hence, it is a good idea to execute time consuming activities on a different Thread as shown by @xmcp

However, if you DO NOT want to use threading, you can use the after method to achieve what you want:

def stop_progressbar(self):
    self.progress.stop()

def traitement(self):
    self.progress.grid()
    self.progress.start()
    self.after(15000, self.stop_progressbar) 
    ## Call Just like you have many, many code lines...

The above code used self.after() method which will execute the stop_progressbar method to stop after 15 seconds, instead of time.sleep() which blocks the mainthread.

Answered By: MightyInSpirit

tqdm is a popular progressbar library that also has experimental support for tkinter (link to API). It is effectively a wrapper for ttk.Progressbar.
The usage is not well documented (and there are obvious bugs) but here is a minimal working example:

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button   

window = Tk()    

pbar = tqdm(total=30, tk_parent=window)    

def run_task():
    for _ in range(30):
        sleep(0.1)
        pbar.update(1)
    pbar.close() # intended usage, might be buggy
    #pbar._tk_window.destroy() # workaround
    
start_button = Button(window, text="Start", command=run_task)
start_button.pack()
    
window.mainloop()

Will produce a progressbar in a separate window looking like this:

enter image description here

Answered By: runDOSrun

Sharing my experiments with TQDM and TK, working off of the answers from the lovely @xmcp and @runDOSrun

Bad example

The progress bar is created and finished in one method – the result is the program seems to hang until a completed progress bar pops up.

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button

window = Tk()

def run_task():
    pbar = tqdm(total=30, tk_parent=window)
    for _ in range(30):
        sleep(0.1)
        pbar.update(1)

start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

Basic working example

Also added lines to disable the button when you press it, and re-enable at the end. If you leave those two lines off, pressing the button again restarts the progress bar.

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button

window = Tk()

pbar = tqdm(tk_parent=window)  # Create the progress bar ahead of time
pbar._tk_window.withdraw()  # Hide it immediately

def run_task():
    start_button['state'] = 'disabled'
    pbar._tk_window.deiconify()
    pbar.reset(total=30)
    for _ in range(30):
        sleep(0.1)
        pbar.update(1)
    pbar._tk_window.withdraw()  # don't close the progress bar, just hide it for next time
    # pbar.close()  # intended usage, might be buggy
    # pbar._tk_window.destroy() # workaround
    start_button['state'] = 'normal'


start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

A single progress bar; the button is disabled
If you leave off the button-disabling step, pressing the button again resets the progress bar.

Threaded example

This one doesn’t make you wait for the progress bar to finish. Button is still active, so you can spawn multiple progress bars if you spam it.

from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button
import threading

window = Tk()

def run_task():
    def threaded_task():
        pbar = tqdm(iterable=range(30), total=30, tk_parent=window)
        for _ in pbar:
            sleep(0.1)
            # pbar.update(1)
        # pbar.close()  # intended usage, might be buggy
        pbar._tk_window.destroy() # workaround
    threading.Thread(target=threaded_task).start()


start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

Multiple progress bars

Complicated example

Uses a ThreadPoolExecutor to spawn several progress bars at once. The main bar should stick to the foreground, and will update as the subprocesses complete.

import random
from tqdm.tk import tqdm
from time import sleep
from tkinter import Tk, Button
import threading
from concurrent.futures import ThreadPoolExecutor

window = Tk()


# pbar = tqdm(total=30, tk_parent=window)
# pbar._tk_window.withdraw()
# pbar._tk_window.deiconify()

def run_task():
    def threaded_task(iterable: []):
        def inner_task(n: int):
            pbar = tqdm(iterable=range(n), total=n, tk_parent=window, desc=threading.current_thread().name, grab=False)
            for _ in pbar:
                sleep(.1)
            # pbar.close()
            pbar._tk_window.destroy()

        with ThreadPoolExecutor(max_workers=8) as tpe:
            pbar = tqdm(tpe.map(inner_task, iterable), total=len(iterable), grab=True, desc="Main progress bar")
            pbar._tk_window.attributes('-topmost', True)  # Keep the main progress bar on top, or it's hard to see
            pbar._tk_window.focus_get()
            list(pbar)
            # Map makes an iterable, but you have to iterate on it to actually show the progress bar
            #    list is a quick and easy iteration.

            # pbar.close()
            pbar._tk_window.destroy()


    threading.Thread(target=threaded_task, kwargs={'iterable': random.sample(range(1, 100), 20)}).start()


start_button = Button(window, text="Start", command=run_task)
start_button.pack()

window.mainloop()

Many progress bars running concurrently

For usability, you’re almost certainly going to want to use the threaded example(s) – the basic working example (re)uses one pre-defined progress bar and is a little more clunky. With the threaded examples, you don’t have to know in advance how many progress bars you’re going to need.

You can absolutely disable the button on the threaded examples too, to prevent multiple concurrent activations.

Answered By: user21504410
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.