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()
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.
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.
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.
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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With