23

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?

wovano
  • 4,543
  • 5
  • 22
  • 49
j666
  • 349
  • 1
  • 2
  • 9

5 Answers5

33

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()
wovano
  • 4,543
  • 5
  • 22
  • 49
xmcp
  • 3,347
  • 2
  • 23
  • 36
  • thanks for the answer. I don't want to show the progresse bar before the user click on the EXE button... Is that i don't understand, how to show the progressbar went you are in -> def sous_ens... Went i try to put progress bar in the code of the fonction sous_ens, it appear only went i get off the fonction... – j666 Nov 18 '15 at 20:53
  • 1
    Just `pack`(or `grid`) the progress bar instance when you want to show it and use `pack_forget` (or `grid_forget`) to hide it. – xmcp Nov 18 '15 at 23:06
  • Thanks a lot. Put i don't catch how to... i put some simple code... How can i put a progress bar went i start the traitement fonction... – j666 Nov 21 '15 at 00:13
  • 2
    your code is almost correct. just use `threading` library and run `traitement` in another thread. see my updated answer. – xmcp Nov 21 '15 at 04:11
  • Just as a side note, this uses the ttk button instead of the tkinter button, since you import ttk after, and it overides the tkinter button class. – Artemis May 02 '18 at 19:09
  • This seems super simple, but I cannot solve where to put the code that should be running while the progress bar is working. Say I just want to write in a loop to the console while the progress bar is running, like this: [[ for i in range(0,200): print(i)]] Where would I put that in the example above for it to run while the progress bar is displayed and working? Thank you! – Korzak Jun 22 '18 at 21:15
  • My thread starts from outside the main class. And there it sows progressBar is not defined. Anyway to figure out that. – Naazneen Jatu Apr 06 '20 at 13:33
  • This answer uses `tkinter` widgets from the second thread which you shouldn't do. Most of the time `tkinter` doesn't allow you to access widgets if you aren't in the thread where the original `tk.Tk()` has been created. – TheLizzard Apr 08 '21 at 13:04
3

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.

MightyInSpirit
  • 145
  • 1
  • 1
  • 9
  • You don't have to have `mainloop` in a tkinter app. You can use a while true loop of updating the window... it also works (except there is an error when you close the window) – YJiqdAdwTifMxGR Dec 28 '20 at 14:17
0

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

runDOSrun
  • 10,359
  • 7
  • 47
  • 57
0

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.

0

Try this -

from tkinter import *
from tkinter import ttk
import requests
from tqdm.tk import tqdm
from functools import partial

root = Tk()
root.geometry("600x500")
root.minsize(145, 50)
root.maxsize(900, 600)

frm = ttk.Frame(root)
frm.grid()

def down():
    # progress = Progressbar(root, orient=HORIZONTAL, length=100, mode='determinate')
    r = requests.get("https://www.win-rar.com/fileadmin/winrar versions/winrar/winrar-x64-621.exe", stream=True)
    totalbyte = int(r.headers['Content-Length'])

    print(totalbyte)
    byte = 0

    progress_bar = tqdm(total=totalbyte, unit='B', unit_scale=True, tk_parent=root)

    with open("win.rar", "wb") as f:
        for chunk in r.iter_content(chunk_size=120):
        progress_bar.update(120)
        f.write(chunk)
        byte += 120
        root.update()

    progress_bar.close()
    progress_bar._tk_window.destroy()
    print(byte)

ttk.Label(frm, text="Hello World!").grid(column=0, row=0)
ttk.Button(frm, text="Download win.rar file", 
command=partial(down)).grid(column=1, row=0)
ttk.Button(frm, text="Quit", command=root.destroy).grid(column=2, row=0)

root.mainloop()
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 18 '23 at 20:23