Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tkinter progress bar how to correctly implement it in a model dialog box

I want to have a toplevel window/dialog box with a progress bar and a few entry, label and button widgets. I want the dialog box to get updated from the main_window window. The main_window does the work and I need this to be reflected in the dialog box. I want the main window to remain active so you can stop the process. I also want to be able to stop the process in the dialog box.

I could not get this to work without using multiprocessing and threading. It seems like I'm going about this the wrong way or am I? Also I'm new to multiprocessing and threading so I hope I did that correctly anyways. If anyone knows of a better way to do this please let me know.

Below is my attempt at doing what I want, it works but is it the correct way to do it?

My First Attempt:

import tkinter as tk
import tkinter.ttk as ttk

from time import sleep
from queue import Empty
from threading import Thread
from multiprocessing import Process, Queue


HIDE = -1
STOP = -2
BREAK = -3
PAUSE = -4
RESUME = -5


class App(tk.Tk):
    def __init__(self, **kwargs):
        title = kwargs.pop('title', '')
        theme = kwargs.pop('theme', 'clam')
        geometry = kwargs.pop('geometry', None)
        exit_callback = kwargs.pop('exit_callback', None)
        super().__init__(**kwargs)

        self.title(title)
        self.style = ttk.Style()
        self.style.theme_use(theme)

        if geometry:
            self.geometry(geometry)

        if exit_callback:
            self.protocol('WM_DELETE_WINDOW', exit_callback)


def main_window(out_que, in_que, maximum):
    def worker():
        if app.running:
            return

        app.running = True
        app.finished = False
        for count in range(0, maximum + 1):
            try:
                message = in_que.get_nowait()
                if message:
                    if message == PAUSE:
                        message = in_que.get()

                    if message == BREAK:
                        break
                    elif message == STOP:
                        app.destroy()
            except Empty:
                pass

            sleep(0.1)  # Simulate work.
            out_que.put(count)

        app.running = False
        app.finished = True
        start_btn.config(state=tk.NORMAL)

    def app_stop():
        out_que.put(STOP)
        app.destroy()

    def test_stop():
        if app.running:
            out_que.put(HIDE)
        elif app.finished:
            out_que.put(HIDE)
            in_que.get()

        stop_btn.config(state=tk.DISABLED)
        start_btn.config(state=tk.NORMAL)

    def test_start():
        while not in_que.empty():
            in_que.get()

        stop_btn.config(state=tk.NORMAL)
        start_btn.config(state=tk.DISABLED)

        thread = Thread(target=worker, daemon=True)
        thread.daemon = True
        thread.start()

    app = App(title='Main Window', theme='alt', geometry='350x150', exit_callback=app_stop)
    app.running = False
    app.finished = True
    app.rowconfigure(0, weight=1)
    app.rowconfigure(1, weight=1)
    app.columnconfigure(0, weight=1)

    start_btn = ttk.Button(app, text='Start Test', command=test_start)
    start_btn.grid(padx=10, pady=5, sticky=tk.NSEW)
    stop_btn = ttk.Button(app, text='Stop Test', state=tk.DISABLED, command=test_stop)
    stop_btn.grid(padx=10, pady=5, sticky=tk.NSEW)

    app.mainloop()


def progress_window(in_que, out_que, maximum):
    def hide():
        out_que.put(BREAK)
        pause_btn.config(text='Pause')
        app.withdraw()

    def pause():
        if progress_bar['value'] < progress_bar['maximum']:
            text = pause_btn.cget('text')
            text = 'Resume' if text == 'Pause' else 'Pause'
            pause_btn.config(text=text)
            out_que.put(PAUSE)
        else:
            pause_btn.config(text='Pause')

    def worker():
        while True:
            data = in_que.get()
            print(data)
            if data == HIDE:
                hide()
            elif data == STOP:
                app.destroy()
                out_que.put(STOP)
                break
            elif not data:
                app.deiconify()
                progress_bar["value"] = 0
            else:
                progress_bar["value"] = data
                app.update_idletasks()

    app = App(title='Progress', theme='clam', geometry='350x150', exit_callback=hide)

    app.rowconfigure(0, weight=1)
    app.rowconfigure(1, weight=1)
    app.columnconfigure(0, weight=1)
    app.columnconfigure(1, weight=1)

    progress_bar = ttk.Progressbar(app, orient=tk.HORIZONTAL, mode='determinate')
    progress_bar["maximum"] = maximum
    progress_bar.grid(padx=10, sticky=tk.EW, columnspan=1000)

    pause_btn = ttk.Button(app, text='Pause', command=pause)
    pause_btn.grid()
    cancel_btn = ttk.Button(app, text='Cancel', command=hide)
    cancel_btn.grid(row=1, column=1)

    thread = Thread(target=worker)
    thread.daemon = True
    thread.start()

    app.withdraw()
    app.mainloop()


if __name__ == '__main__':
    jobs = []
    que1 = Queue()
    que2 = Queue()
    process = 50  # The maximum amount of work to process, # items.

    for target in (main_window, progress_window):
        p = Process(target=target, args=(que1, que2, process))
        jobs.append(p)
        p.start()

    for j in jobs:
        j.join()

Here is my second attempt, without multiprocessing just threading.

I have updated the code to not use multiprocessing, just threading. Is threading necessary or can the be done with out it and accomplish the same thing?

The code seems to work fine but am I doing it right? I'm new to threading and just want to make sure I'm doing things right before I continue with my project.

import tkinter as tk
import tkinter.ttk as ttk

from time import sleep
from queue import Empty
from threading import Thread
from multiprocessing import Queue


HIDE = -1
STOP = -2
DONE = -3
BREAK = -4
PAUSE = -5


class App(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.running = False
        self.finished = True
        self.app_que = Queue()
        self.dialog_que = Queue()
        self.process_items = 50

        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)

        self.title('Main Window')
        self.geometry('350x150')

        self.style = ttk.Style()
        self.style.theme_use('clam')

        wdg = self.start_btn = ttk.Button(self, text='Start Test', command=self.test_start)
        wdg.grid(padx=10, pady=5, sticky=tk.NSEW)
        wdg = self.stop_btn = ttk.Button(self, text='Stop Test', state=tk.DISABLED, command=self.test_stop)
        wdg.grid(padx=10, pady=5, sticky=tk.NSEW)

        self.dlg = ProgressDialog(self, title='Progress', geometry='350x150', process=self.process_items)
        self.dlg.app_que = self.app_que
        self.dlg.dialog_que = self.dialog_que

        self.protocol('WM_DELETE_WINDOW', self.app_stop)

        thread = Thread(target=self.dlg.worker, daemon=True)
        thread.start()

    def worker(self):
        self.dlg.cancel_btn.config(text='Cancel')
        self.dlg.pause_btn.config(state=tk.NORMAL)

        for count in range(0, self.process_items + 1):
            try:
                message = self.app_que.get_nowait()
                if message:
                    if message == PAUSE:
                        message = self.app_que.get()

                    if message == BREAK:
                        self.stop_btn.config(state=tk.DISABLED)
                        break
                    elif message == STOP:
                        self.destroy()
            except Empty:
                pass

            sleep(0.1)  # Simulate work.
            self.dialog_que.put(count)

        self.dialog_que.put(DONE)
        self.dlg.cancel_btn.config(text='Close')

        self.finished = True
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)

    def app_stop(self):
        self.dialog_que.put(STOP)
        self.destroy()

    def test_stop(self):
        if self.running or self.finished:
            self.dialog_que.put(HIDE)

        self.stop_btn.config(state=tk.DISABLED)
        self.start_btn.config(state=tk.NORMAL)

    def test_start(self):
        while not self.app_que.empty():
            self.app_que.get()

        thread = Thread(target=self.worker, daemon=True)
        thread.start()

        self.stop_btn.config(state=tk.NORMAL)
        self.start_btn.config(state=tk.DISABLED)

        self.dlg.deiconify()


class ProgressDialog(tk.Toplevel):
    def __init__(self, parent, *args, **kwargs):
        title = kwargs.pop('title', '')
        process = kwargs.pop('process', 0)
        geometry = kwargs.pop('geometry', None)
        super().__init__(parent, *args, **kwargs)
        self.withdraw()

        self.app_que = None
        self.dialog_que = None

        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)

        self.title(title)

        if geometry:
            self.geometry(geometry)

        wdg = self.progress_bar = ttk.Progressbar(self, orient=tk.HORIZONTAL, mode='determinate')
        wdg["value"] = 0
        wdg["maximum"] = process
        wdg.grid(padx=10, sticky=tk.EW, columnspan=1000)

        wdg = self.pause_btn = ttk.Button(self, text='Pause', command=self.pause)
        wdg.grid()
        wdg = self.cancel_btn = ttk.Button(self, text='Cancel', command=self.hide)
        wdg.grid(row=1, column=1)

        self.protocol('WM_DELETE_WINDOW', self.hide)

    def worker(self):
        while True:
            message = self.dialog_que.get()
            print(message)
            if message == HIDE:
                self.hide()
            elif message == STOP:
                self.app_que.put(DONE)
                break
            elif message == DONE:
                self.pause_btn.config(state=tk.DISABLED)
            else:
                self.progress_bar["value"] = message

    def hide(self):
        self.app_que.put(BREAK)
        self.pause_btn.config(text='Pause')
        self.withdraw()

    def pause(self):
        if self.progress_bar['value'] < self.progress_bar['maximum']:
            text = self.pause_btn.cget('text')
            text = 'Resume' if text == 'Pause' else 'Pause'
            self.pause_btn.config(text=text)
            self.app_que.put(PAUSE)
        else:
            self.pause_btn.config(text='Pause')


if __name__ == '__main__':
    app = App()
    app.mainloop()
like image 433
Daniel Huckson Avatar asked May 19 '20 17:05

Daniel Huckson


People also ask

How do I use the progress bar in tkinter?

Use the ttk. Progressbar(container, orient, length, mode) to create a progressbar. Use the indeterminate mode when the program cannot accurately know the relative progress to display. Use the determinate mode if you know how to measure the progress accurately.

How do you make a circular progress bar in tkinter?

Tkinter does not have any support for circular progress bars. You will have to draw your own using a series of images, or a drawing on a canvas.

Which function is used to create a ProgressBar in tkinter?

We will create a full-width progress bar by using ProgressBar(win, options) method. It can be configured through a button that enables and disables it.

How do I open a dialog box in tkinter?

Use the askopenfilename() function to display an open file dialog that allows users to select one file. Use the askopenfilenames() function to display an open file dialog that allows users to select multiple files.


Video Answer


1 Answers

If you didn't want to use thread,maybe you could try asyncio.I don't know whether my code is correct,but it works fine on my PC.

Welcome to point out the fault in my code, I really don't know whether it is a good practice.

import tkinter as tk
from tkinter import ttk
import asyncio, time
import warnings


class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()

        self.start_btn = ttk.Button(self, text="Start Test", command=self.test_start)
        self.start_btn.pack(padx=10, pady=5, fill="both", expand=True)

        self.stop_btn = ttk.Button(self, text="Stop Test", command=self.test_stop, state=tk.DISABLED)
        self.stop_btn.pack(padx=10, pady=5, fill="both", expand=True)

        self.test_window = tk.Toplevel()
        self.test_window.progressbar = ttk.Progressbar(self.test_window, orient=tk.HORIZONTAL)
        self.test_window.progressbar.grid(padx=10, pady=5, sticky=tk.NSEW, columnspan=2, column=0, row=0)

        self.test_window.switch_btn = ttk.Button(self.test_window, text="Pause", command=self.switch)
        self.test_window.switch_btn.grid(padx=10, pady=5, sticky=tk.NSEW, column=0, row=1)
        self.test_window.cancel_btn = ttk.Button(self.test_window, text="Cancel", command=self.test_cancel)
        self.test_window.cancel_btn.grid(padx=10, pady=5, sticky=tk.NSEW, column=1, row=1)

        self.test_window.withdraw()

    def test_start(self):
        self.stop_btn['state'] = tk.NORMAL
        self.test_window.deiconify()
        self.test_window.after(0, self.work)

    def work(self):
        async def async_work(): # define a async task
            try:
                await asyncio.sleep(3)  # could be another async work.
            except asyncio.CancelledError:
                print("cancel or stop")
                raise  # if don't raise the error ,it won't cancel

        async def progressbar_add():
            self.task = asyncio.create_task(async_work())
            timeout = 0
            while True: # wait the async task finish
                done, pending = await asyncio.wait({self.task}, timeout=timeout)
                self.test_window.update()
                if self.task in done:
                    self.test_window.progressbar['value'] += 10  # if finished, value += 10
                    print(self.test_window.progressbar['value'])
                    await self.task
                    break

        if self.test_window.progressbar['value'] >= 100:
            return
        asyncio.run(progressbar_add())
        self.test_window.after(0, self.work)

    def test_stop(self):
        self.test_window.progressbar['value'] = 0
        self.stop_btn['state'] = tk.DISABLED
        try:
            all_tasks = asyncio.Task.all_tasks()
            for task in all_tasks:
                task.cancel()
        except RuntimeError:  # if you have cancel the task it will raise RuntimeError
            pass

    def switch(self):
        if self.test_window.switch_btn['text'] == 'Pause':
            self.test_window.switch_btn['text'] = 'Resume'
            try:
                all_tasks = asyncio.Task.all_tasks()
                for task in all_tasks:
                    task.cancel()
            except RuntimeError:  # if you have cancel the task it will raise RuntimeError
                pass
        else:
            self.test_window.switch_btn['text'] = 'Pause'
            return self.work()

    def test_cancel(self):
        # self.test_window.progressbar['value'] = 0
        print(self.test_window.progressbar['value'])
        self.test_window.withdraw()
        self.task.cancel()


app = App()
app.mainloop()

Below Python 3.7,you couldn't use asyncio.run(async).It was added in Python 3.7.Need to use get_event_loop() and run_until_complete().(Point out By @Saad)

like image 70
jizhihaoSAMA Avatar answered Oct 17 '22 06:10

jizhihaoSAMA