Updating a tk ProgressBar from a multiprocess.proccess in python3 - multithreading

I have successfully created a threading example of a thread which can update a Progressbar as it goes. However doing the same thing with multiprocessing has so far eluded me.
I'm beginning to wonder if it is possible to use tkinter in this way. Has anyone done this?
I am running on OS X 10.7. I know from looking around that different OS's may behave very differently, especially with multiprocessing and tkinter.
I have tried a producer which talks directly to the widget, through both namespaces and event.wait, and event.set. I have done the same thing with a producer talking to a consumer which is either a method or function which talks to the widget. All of these things successfully run, but do not update the widget visually. Although I have done a get() on the IntVar the widget is bound to and seen it change, both when using widget.step() and/or widget.set(). I have even tried running a separate tk() instance inside the sub process. Nothing updates the Progressbar.
Here is one of the simpler versions. The sub process is a method on an object that is a wrapper for the Progressbar widget. The tk GUI runs as the main process. I also find it a little odd that the widget does not get destroyed at the end of the loop, which is probably a clue I'm not understanding the implications of.
import multiprocessing
from tkinter import *
from tkinter import ttk
import time
root = Tk()
class main_window:
def __init__(self):
self.dialog_count = 0
self.parent = root
self.parent.title('multiprocessing progess bar')
frame = ttk.Labelframe(self.parent)
frame.pack(pady=10, padx=10)
btn = ttk.Button(frame, text="Cancel")
btn.bind("<Button-1>", self.cancel)
btn.grid(row=0, column=1, pady=10)
btn = ttk.Button(frame, text="progress_bar")
btn.bind("<Button-1>", self.pbar)
btn.grid(row=0, column=2, pady=10)
self.parent.mainloop()
def pbar(self, event):
name="producer %d" % self.dialog_count
self.dialog_count += 1
pbar = pbar_dialog(self.parent, title=name)
event = multiprocessing.Event()
p = multiprocessing.Process(target=pbar.consumer, args=(None, event))
p.start()
def cancel(self, event):
self.parent.destroy()
class pbar_dialog:
toplevel=None
pbar_count = 0
def __init__(self, parent, ns=None, event=None, title=None, max=100):
self.ns = ns
self.pbar_value = IntVar()
self.max = max
pbar_dialog.pbar_count += 1
self.pbar_value.set(0)
if not pbar_dialog.toplevel:
pbar_dialog.toplevel= Toplevel(parent)
self.frame = ttk.Labelframe(pbar_dialog.toplevel, text=title)
#self.frame.pack()
self.pbar = ttk.Progressbar(self.frame, length=300, variable=self.pbar_value)
self.pbar.grid(row=0, column=1, columnspan=2, padx=5, pady=5)
btn = ttk.Button(self.frame, text="Cancel")
btn.bind("<Button-1>", self.cancel)
btn.grid(row=0, column=3, pady=10)
self.frame.pack()
def set(self,value):
self.pbar_value.set(value)
def step(self,increment=1):
self.pbar.step(increment)
print ("Current", self.pbar_value.get())
def cancel(self, event):
self.destroy()
def destroy(self):
self.frame.destroy()
pbar_dialog.pbar_count -= 1
if pbar_dialog.pbar_count == 0:
pbar_dialog.toplevel.destroy()
def consumer(self, ns, event):
for i in range(21):
#event.wait(2)
self.step(5)
#self.set(i)
print("Consumer", i)
self.destroy()
if __name__ == '__main__':
main_window()
For contrast, here is the threading version which works perfectly.
import threading
from tkinter import *
from tkinter import ttk
import time
root = Tk()
class main_window:
def __init__(self):
self.dialog_count = 0
self.parent = root
self.parent.title('multiprocessing progess bar')
frame = ttk.Labelframe(self.parent)
frame.pack(pady=10, padx=10)
btn = ttk.Button(frame, text="Cancel")
btn.bind("<Button-1>", self.cancel)
btn.grid(row=0, column=1, pady=10)
btn = ttk.Button(frame, text="progress_bar")
btn.bind("<Button-1>", self.pbar)
btn.grid(row=0, column=2, pady=10)
self.parent.mainloop()
def producer(self, pbar):
i=0
while i < 101:
time.sleep(1)
pbar.step(1)
i += 1
pbar.destroy()
def pbar(self, event):
name="producer %d" % self.dialog_count
self.dialog_count += 1
pbar = pbar_dialog(self.parent, title=name)
p = threading.Thread(name=name, target=self.producer, args=(pbar,))
p.start()
#p.join()
def cancel(self, event):
self.parent.destroy()
class pbar_dialog:
toplevel=None
pbar_count = 0
def __init__(self, parent, ns=None, event=None, title=None, max=100):
self.ns = ns
self.pbar_value = IntVar()
self.title = title
self.max = max
pbar_dialog.pbar_count += 1
if not pbar_dialog.toplevel:
pbar_dialog.toplevel= Toplevel(parent)
self.frame = ttk.Labelframe(pbar_dialog.toplevel, text=title)
#self.frame.pack()
self.pbar = ttk.Progressbar(self.frame, length=300, variable=self.pbar_value)
self.pbar.grid(row=0, column=1, columnspan=2, padx=5, pady=5)
btn = ttk.Button(self.frame, text="Cancel")
btn.bind("<Button-1>", self.cancel)
btn.grid(row=0, column=3, pady=10)
self.frame.pack()
self.set(0)
def set(self,value):
self.pbar_value.set(value)
def step(self,increment=1):
self.pbar.step(increment)
def cancel(self, event):
self.destroy()
def destroy(self):
self.frame.destroy()
pbar_dialog.pbar_count -= 1
if pbar_dialog.pbar_count == 0:
pbar_dialog.toplevel.destroy()
pbar_dialog.toplevel = None
def automatic(self, ns, event):
for i in range(1,100):
self.step()
if __name__ == '__main__':
main_window()

Doing something similar, I ended up having to use a combination of threads and processes - the GUI front end had two threads: one for tkinter, and one reading from a multiprocessing.Queue and calling gui.update() - then the back-end processes would write updates into that Queue

This might be a strange approach, but it works for me. Copy and paste this code to a file and run it to see the result. It's ready to run.
I don't have the patience to explain my code right now, I might edit it another day.
Oh, and this is in Python 2.7 I started programming two months ago, so I have not idea if the difference is relevant.
# -*- coding: utf-8 -*-
# threadsandprocesses.py
# Importing modules
import time
import threading
import multiprocessing
import Tkinter as tki
import ttk
class Master(object):
def __init__(self):
self.mainw = tki.Tk()
self.mainw.protocol("WM_DELETE_WINDOW", self.myclose)
self.mainw.title("Progressbar")
self.mainw.geometry('300x100+300+300')
self.main = tki.Frame(self.mainw)
self.RunButton = ttk.Button(self.main, text='Run',
command=self.dostuff)
self.EntryBox = ttk.Entry(self.main)
self.EntryBox.insert(0, "Enter a number")
self.progress = ttk.Progressbar(self.main,
mode='determinate', value=0)
self.main.pack(fill=tki.BOTH, expand=tki.YES)
self.progress.pack(expand=tki.YES)
self.EntryBox.pack(expand=tki.YES)
self.RunButton.pack()
print "The Master was created"
def dostuff(self):
print "The Master does no work himself"
data = range(int(self.EntryBox.get()))
S = Slave(self, data)
print "The Master created a Slave to do his stuff"
print "The Slave gets told to start his work"
S.start()
def myclose(self):
self.mainw.destroy()
return
def nextstep(self):
print "Good job, Slave, I see the result is"
print Master.results.get()
class Slave(threading.Thread):
def __init__(self, guest, data):
print "This is the Slave."
print "Nowdays, Work is outsourced!"
self.data = data
self.guest = guest
threading.Thread.__init__(self)
def run(self):
print "The Slave is outsourcing his work to Calcualte inc."
time.sleep(1)
Outsourcing = Calculate()
Results = Outsourcing.run(self.guest, self.data)
return Results
# unwrapping outside a class
def calc(arg, **kwarg):
return Calculate.calculate(*arg, **kwarg)
class Calculate(object):
def run(self, guest, data):
print"This is Calculate inc. ... how can I help you?"
time.sleep(1)
maximum = int(guest.EntryBox.get())
guest.progress.configure(maximum=maximum, value=0)
manager = multiprocessing.Manager()
queue = manager.Queue()
lock = manager.Lock()
print "Things are setup and good to go"
# Counting the number of available CPUs in System
pool_size = multiprocessing.cpu_count()
print "Your system has %d CPUs" % (pool_size)
# Creating a pool of processes with the maximal number of CPUs possible
pool = multiprocessing.Pool(processes=pool_size)
Master.results = pool.map_async(calc, (zip([self]*len(data), [lock]*len(data),
[queue]*len(data), data)))
for job in range(1, maximum+1):
queue.get() # this is an abuse I think, but works for me
guest.progress.configure(value=job)
# Properly close and end all processes, once we're done
pool.close()
pool.join()
print "All done"
guest.nextstep()
return
def calculate(self, lock, queue, indata):
lock.acquire()
print 'Reading values and starting work'
lock.release()
time.sleep(3) # some work
results = indata # The works results
lock.acquire()
print 'Done'
lock.release()
queue.put("Finished!")
return results
if __name__ == '__main__':
TheMaster = Master()
TheMaster.mainw.mainloop()

Related

Is there a way to make tkinter windows work independantly Tkinter?

I have been looking to create a code that opens a second tkinter window to display stuffs live while a program is running on my main window. However, doing so, my main window gets frozen during 5s and then displays stuff on my second window when it is completed.
Is there a way to live display in the second window ?
My code below:
import tkinter as tk
from tkinter import ttk
import time
class PopUpLog(tk.Tk):
def __init__(self, parent):
tk.Tk.__init__(self)
self.y=5
tk.Button(self.master, text="Write in pop-up", command=self.write).pack(side="left")
# canvas
frameL = tk.Frame(self)
frameL.pack(side="left", fill="both")
self.canvasL = tk.Canvas(frameL, height=800, width=800)
self.canvasL.pack(fill="both", expand=True)
# scrollbar
vsb = ttk.Scrollbar(self, orient="v", command=self.canvasL.yview)
vsb.pack(side="left", fill="y")
self.canvasL.configure(yscrollcommand=vsb.set)
self.canvasL.bind("<Configure>", lambda e:self.canvasL.configure(scrollregion=self.canvasL.bbox("all")))
def write(self, text="hi im a pop-up"):
for i in range(5):
self.canvasL.create_text(5, self.y, anchor='nw', justify='left', text=text)
self.y += 25
time.sleep(1)
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
tk.Button(self, text="Open window", command=self.popup).pack(side="left")
def popup(self):
self.top = PopUpLog(self)
self.top.geometry("400x400")
self.top.title("pop-up")
self.top.mainloop()
if __name__ == "__main__":
root = App()
root.mainloop()
So far, the program runs for 5s and then displays everything in self.top. BUT I need a live display (made every time create_text is called) in self.top but I can't even get that.
I am sorry if this is redundant to another question asked but I couldn't find helpful enough information.
Thanks a lot !
time.sleep is the reason why your window is freezing. This is the case for virtually any GUI toolkit. If you want the updates to happen incrementally you can use the after method which will execute the callback you assign after a certain number of milliseconds.
Also there should only be one mainloop. There is no need to start one per window and doing so could cause problems.
Here is an example using the after method:
class PopUpLog(tk.Tk):
def __init__(self, parent):
tk.Tk.__init__(self)
self.y=5
self.c=5 # counter
tk.Button(self.master, text="Write in pop-up", command=self.write).pack(side="left")
# canvas
frameL = tk.Frame(self)
frameL.pack(side="left", fill="both")
self.canvasL = tk.Canvas(frameL, height=800, width=800)
self.canvasL.pack(fill="both", expand=True)
# scrollbar
vsb = ttk.Scrollbar(self, orient="v", command=self.canvasL.yview)
vsb.pack(side="left", fill="y")
self.canvasL.configure(yscrollcommand=vsb.set)
self.canvasL.bind("<Configure>", lambda e:self.canvasL.configure(scrollregion=self.canvasL.bbox("all")))
def write(self, text="hi im a pop-up"):
if self.c > 0:
self.canvasL.create_text(5, self.y, anchor='nw', justify='left', text=text)
self.y += 25
self.c -= 1 # reduce counter
self.after(1000, self.write) # call again in 1 second
else:
self.c = 5 # when counter is 0 reset counter
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
tk.Button(self, text="Open window", command=self.popup).pack(side="left")
def popup(self):
self.top = PopUpLog(self)
self.top.geometry("400x400")
self.top.title("pop-up")
if __name__ == "__main__":
root = App()
root.mainloop()

Catch macos ⌘Q in python tkinter application

eWithin a tkinter application I am catching several events to gracefully shutdown some threads within the application, before the main thread terminates.
This is all working swiftly as long as I use a bound key combination or the
window control, the cross in the red circle.
On macos the application automatically gets a 'python' menu with a close function bound to key combination ⌘Q. This event is not handled properly. It seems to kill the main thread but other threads are not closed properly.
Following bindings are used to catch all closing events:
self.root.bind('<Control-x>', self.exitapp)
self.root.protocol("WM_DELETE_WINDOW", self.exitapp)
atexit.register(self.catch_atexit)
Recently found that the left and right ⌘ keys are repesented as Meta_L and Meta_R but cannot be combined with a second key, i.e. '<Meta_L-q>'.
Can anyone explain howto catch ⌘Q?
Please find code example below:
#!/usr/bin/env python3
import sys
from tkinter import *
from tkinter import ttk
import threading
import time
import atexit
class subthread():
def __init__(self):
self.thr = None
self.command = ''
self.proof = ""
def start(self):
if not self.thr or not self.thr.is_alive():
self.command = 'run'
self.thr = threading.Thread(target=self.loop)
self.thr.start()
else:
print('thread already running')
def stop(self):
self.command = 'stop'
if self.thr and self.thr.is_alive():
print('stopping thread')
else:
print('thread not running')
def running(self):
return True if self.thr and self.thr.is_alive() else False
def get_proof(self):
return self.proof
def loop(self):
while self.command == 'run':
time.sleep(0.5)
print('+', end='')
self.proof += '+'
if len(self.proof) > 30:
self.proof = ""
def __del__(self):
print('del instance subthread')
self.command = 'stop'
if self.thr and self.thr.is_alive():
self.thr.join(2)
class app():
def __init__(self, rootframe):
self.root = rootframe
self.gui = ttk.Frame(self.root)
self.gui.pack(fill=BOTH)
row = 0
self.checkvar = IntVar()
self.checkvar.trace('w', self.threadchange)
ttk.Label(self.gui, text="Use checkbox to start and stop thread").grid(row=row, column=0, columnspan=2)
ttk.Checkbutton(self.gui, text='thread', variable=self.checkvar).grid(row=1, column=0)
self.threadstatus = StringVar()
self.threadstatus.set('not running')
row += 1
ttk.Label(self.gui, textvariable=self.threadstatus).grid(row=row, column=1)
row += 1
self.alivestring = StringVar()
ttk.Entry(self.gui, textvariable=self.alivestring).grid(row=row, column=0, padx=10, sticky="ew",
columnspan=3)
row += 1
ttk.Separator(self.gui, orient="horizontal").grid(row=row, column=0, padx=10, sticky="ew",
columnspan=3)
row += 1
ttk.Label(self.gui, text="- Available options to close application: [ctrl]-x,"
" window-control-red, [CMD]-q").grid(row=row, column=0, padx=10, columnspan=3)
row += 1
ttk.Label(self.gui, text="1. Try all three without thread running").grid(row=row, column=0,
columnspan=3, sticky='w')
row += 1
ttk.Label(self.gui, text="2. Retry all three after first starting the thread").grid(row=row, column=0,
columnspan=3, sticky='w')
row += 1
ttk.Label(self.gui, text="3. Experience that only [CMD]-q fails").grid(row=row, column=0,
columnspan=3, sticky='w')
self.subt = subthread()
self.root.bind('<Control-x>', self.exitapp1)
self.root.protocol("WM_DELETE_WINDOW", self.exitapp2)
atexit.register(self.catch_atexit)
self.root.after(500, self.updategui)
def threadchange(self, a, b, c):
""" checkbox change handler """
try:
if self.checkvar.get() == 1:
self.subt.start()
else:
self.subt.stop()
except Exception as ex:
print('failed to control subt', str(ex))
def updategui(self):
""" retriggering timer handler to update status label gui """
try:
if self.subt.running():
self.threadstatus.set("thread is running")
else:
self.threadstatus.set("thread not running")
self.alivestring.set(self.subt.get_proof())
except:
pass
else:
self.root.after(500, self.updategui)
def __del__(self):
print('app del called')
def exitapp1(self, a):
print('exitapp1 called')
self.subt.stop()
sys.exit(0)
def exitapp2(self):
print('exitapp2 called')
self.subt.stop()
sys.exit(0)
def catch_atexit(self):
print('exitapp called')
self.subt.stop()
self.subt = None
sys.exit(0)
if __name__ == '__main__':
root = Tk()
dut = app(rootframe=root)
root.mainloop()
print('main exiting')
sys.exit(0)
You can catch ⌘Q with <Command-q>:
...
def action(event):
print("bind!")
root.bind_all("<Command-q>", action)
...
This worked for me on macOS HightSierra
#EddyHoogevorst Yes, it does not work on Big Sur.
The code that works is:
root.createcommand("::tk::mac::Quit", action).
NB: need to change function signature for action as event is not passed as an argument in this context.

Tkinter buttons not changing back to the correct color after state changing to active

I am making this PDF tool, and I want the buttons to be disabled until a file or files are successfully imported. This is what the app looks like at the launch:
Right after running the callback for the import files button, the active state looks like this:
I want the colors of the buttons to turn maroon instead of the original grey. They only turn back to maroon once you hover the mouse over them. Any thoughts for how to fix this? Here is the callback for the import button:
def import_callback():
no_files_selected = False
global files
files = []
try:
ocr_button['state'] = DISABLED
merge_button['state'] = DISABLED
status_label.pack_forget()
frame.pack_forget()
files = filedialog.askopenfilenames()
for f in files:
name, extension = os.path.splitext(f)
if extension != '.pdf':
raise
if not files:
no_files_selected = True
raise
if frame.winfo_children():
for label in frame.winfo_children():
label.destroy()
make_import_file_labels(files)
frame.pack()
ocr_button['state'] = ACTIVE
merge_button['state'] = ACTIVE
except:
if no_files_selected:
status_label.config(text='No files selected.', fg='blue')
else:
status_label.config(text='Error: One or more files is not a PDF.', fg='red')
status_label.pack(expand='yes')
import_button = Button(root, text='Import Files', width=scaled(20), bg='#5D1725', bd=0, fg='white', relief='groove',
command=import_callback)
import_button.pack(pady=scaled(50))
I know this was asked quite a while ago, so probably already solved for the user. But since I had the exact same problem and do not see the "simplest" answer here, I thought I would post:
Just change the state from "active" to "normal"
ocr_button['state'] = NORMAL
merge_button['state'] = NORMAL
I hope this helps future users!
As I understand you right you want something like:
...
ocr_button['state'] = DISABLED
ocr_button['background'] = '#*disabled background*'
ocr_button.bind('<Enter>', lambda e:ocr_button.configure(background='#...'))
ocr_button.bind('<Leave>', lambda e:ocr_button.configure(background='#...'))
merge_button['state'] = DISABLED
merge_button['background'] = '#*disabled background*'
merge_button.bind('<Enter>', lambda e:ocr_button.configure(background='#...'))
merge_button.bind('<Leave>', lambda e:ocr_button.configure(background='#...'))
...
...
ocr_button['state'] = ACTIVE
ocr_button['background'] = '#*active background*'
ocr_button.unbind('<Enter>')
ocr_button.unbind('<Leave>')
merge_button['state'] = ACTIVE
merge_button['background'] = '#*active background*'
merge_button.unbind('<Enter>')
merge_button.unbind('<Leave>')
...
If there are any errors, since I wrote it out of my mind or something isnt clear, let me know.
Update
the following code reproduces the behavior as you stated. The reason why this happens is how tkinter designed the standart behavior. You will have a better understanding of it if you consider style of ttk widgets. So I would recommand to dont use the automatically design by state rather write a few lines of code to configure your buttons how you like, add and delete the commands and change the background how you like. If you dont want to write this few lines you would be forced to use ttk.Button and map a behavior you do like
import tkinter as tk
root = tk.Tk()
def func_b1():
print('func of b1 is running')
def disable_b1():
b1.configure(bg='grey', command='')
def activate_b1():
b1.configure(bg='red', command=func_b1)
b1 = tk.Button(root,text='B1', bg='red',command=func_b1)
b2 = tk.Button(root,text='disable', command=disable_b1)
b3 = tk.Button(root,text='activate',command=activate_b1)
b1.pack()
b2.pack()
b3.pack()
root.mainloop()
I've wrote this simple app that I think could help all to reproduce the problem.
Notice that the state of the button when you click is Active.
#!/usr/bin/python3
import sys
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
class Main(ttk.Frame):
def __init__(self, parent, *args, **kwargs):
super().__init__()
self.parent = parent
self.init_ui()
def cols_configure(self, w):
w.columnconfigure(0, weight=0, minsize=100)
w.columnconfigure(1, weight=0)
w.rowconfigure(0, weight=0, minsize=50)
w.rowconfigure(1, weight=0,)
def get_init_ui(self, container):
w = ttk.Frame(container, padding=5)
self.cols_configure(w)
w.grid(row=0, column=0, sticky=tk.N+tk.W+tk.S+tk.E)
return w
def init_ui(self):
w = self.get_init_ui(self.parent)
r = 0
c = 0
b = ttk.LabelFrame(self.parent, text="", relief=tk.GROOVE, padding=5)
self.btn_import = tk.Button(b,
text="Import Files",
underline=1,
command = self.on_import,
bg='#5D1725',
bd=0,
fg='white')
self.btn_import.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
self.parent.bind("<Alt-i>", self.switch)
r +=1
self.btn_ocr = tk.Button(b,
text="OCR FIles",
underline=0,
command = self.on_ocr,
bg='#5D1725',
bd=0,
fg='white')
self.btn_ocr["state"] = tk.DISABLED
self.btn_ocr.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
r +=1
self.btn_merge = tk.Button(b,
text="Merge Files",
underline=0,
command = self.on_merge,
bg='#5D1725',
bd=0,
fg='white')
self.btn_merge["state"] = tk.DISABLED
self.btn_merge.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
r +=1
self.btn_reset = tk.Button(b,
text="Reset",
underline=0,
command = self.switch,
bg='#5D1725',
bd=0,
fg='white')
self.btn_reset.grid(row=r, column=c, sticky=tk.N+tk.W+tk.E,padx=5, pady=5)
b.grid(row=0, column=1, sticky=tk.N+tk.W+tk.S+tk.E)
def on_import(self, evt=None):
self.switch()
#simulate some import
self.after(5000, self.switch())
def switch(self,):
state = self.btn_import["state"]
if state == tk.ACTIVE:
self.btn_import["state"] = tk.DISABLED
self.btn_ocr["state"] = tk.NORMAL
self.btn_merge["state"] = tk.NORMAL
else:
self.btn_import["state"] = tk.NORMAL
self.btn_ocr["state"] = tk.DISABLED
self.btn_merge["state"] = tk.DISABLED
def on_ocr(self, evt=None):
state = self.btn_ocr["state"]
print ("ocr button state is {0}".format(state))
def on_merge(self, evt=None):
state = self.btn_merge["state"]
print ("merge button state is {0}".format(state))
def on_close(self, evt=None):
self.parent.on_exit()
class App(tk.Tk):
"""Main Application start here"""
def __init__(self, *args, **kwargs):
super().__init__()
self.protocol("WM_DELETE_WINDOW", self.on_exit)
self.set_style()
self.set_title(kwargs['title'])
Main(self, *args, **kwargs)
def set_style(self):
self.style = ttk.Style()
#('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')
self.style.theme_use("clam")
def set_title(self, title):
s = "{0}".format('Simple App')
self.title(s)
def on_exit(self):
"""Close all"""
if messagebox.askokcancel(self.title(), "Do you want to quit?", parent=self):
self.destroy()
def main():
args = []
for i in sys.argv:
args.append(i)
kwargs = {"style":"clam", "title":"Simple App",}
app = App(*args, **kwargs)
app.mainloop()
if __name__ == '__main__':
main()

How to use multithreading with tkinter to update a label and simultaneously perform calculations in a thread controlled by button events?

I'm trying to start a counter that displays the value in a label on a separate display window.
The main window has a START, STOP and DISPLAY button.
The START must start the counter, STOP must stop it and the display window must open only when I click on DISPLAY button.
Here's what I have so far. The buttons seem to be unresponsive and the display window pops up without user intervention. How can I fix this?
import tkinter as tk
import time
import threading
import queue
def Run_Device(disp_q,flaq_q):
temp_q = queue.Queue()
temp_q.put(0)
while(flaq_q.empty()):
#time.sleep(0.2)
count = temp_q.get()
count += 1
temp_q.put(count)
disp_q.put(count)
else:
flaq_q.queue.clear()
def P_Window(disp_q):
pw = tk.Tk()
value_label = tk.Label(pw, text=disp_q.get(), relief='sunken', bg='lemon chiffon', font='Helvetica 16 bold')
value_label.pack()
def update_values():
value_label.config(text=disp_q.get())
value_label.after(1000,update_values)
update_values()
pw.mainloop()
def Stop_Dev(flaq_q):
flaq_q.put("Stop")
if __name__ == "__main__":
disp_q = queue.Queue()
flaq_q = queue.Queue()
t_device = threading.Thread(target=Run_Device, args=(disp_q, flaq_q), name="Device 1")
t_disp = threading.Thread(target=P_Window, args=(disp_q, ), name="Display 1")
window = tk.Tk()
start_button = tk.Button(window, text='Start', command=t_device.start(), bg='spring green', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
start_button.pack()
stop_button = tk.Button(window, text='Stop', command=lambda: Stop_Dev(flaq_q), bg='OrangeRed2', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
stop_button.pack()
disp_param_button = tk.Button(window, text='Display', command=t_disp.start(), bg='sky blue', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
disp_param_button.pack()
window.mainloop()
I'm trying to learn how to use multithreading in tkinter so any feedback would be appreciated
There are two issues I see with your code. The first is just simple bugs, e.g.:
start_button = tk.Button(window, text='Start', command=t_device.start(), ...
here it should be command=t_device.start otherwise the command is the function returned by t_device.start()
The second is you've not dealt with various "What if ...?" scenarios, e.g. What if the user pushes 'Start' or 'Display' multiple times?
I've tried to address the above in my rework below:
import tkinter as tk
from time import sleep
from queue import Queue, Empty
from threading import Thread
FONT = 'Helvetica 16 bold'
def run_device():
count = 0
while flaq_q.empty():
count += 1
disp_q.put(count)
sleep(0.5)
while not flaq_q.empty(): # flaq_q.queue.clear() not documented
flaq_q.get(False)
def p_window():
global pw
if pw is None:
pw = tk.Toplevel()
value_label = tk.Label(pw, text=disp_q.get(), width=10, font=FONT)
value_label.pack()
def update_values():
if not disp_q.empty():
try:
value_label.config(text=disp_q.get(False))
except Empty:
pass
pw.after(250, update_values)
update_values()
elif pw.state() == 'normal':
pw.withdraw()
else:
pw.deiconify()
def stop_device():
if flaq_q.empty():
flaq_q.put("Stop")
def start_device():
global device
if device and device.is_alive():
return
while not disp_q.empty():
disp_q.get(False)
disp_q.put(0)
device = Thread(target=run_device)
device.start()
if __name__ == "__main__":
disp_q = Queue()
flaq_q = Queue()
root = tk.Tk()
pw = None
device = None
tk.Button(root, text='Start', command=start_device, width=20, font=FONT).pack()
tk.Button(root, text='Stop', command=stop_device, width=20, font=FONT).pack()
tk.Button(root, text='Display', command=p_window, width=20, font=FONT).pack()
root.mainloop()
I've left out some details to simplify the example. Even with its additional checks, it's still nowhere near perfect. (E.g. it hangs if you don't 'Stop' before you close the window, etc.)
You might be able to adapt something like the following:
import tkinter as tk
import threading
import queue
import time
def run_device():
for i in range(4):
print(i)
pass
def process_queue(MyQueue):
"""Check if we got a complete message from our thread.
Start the next operation if we are ready"""
try:
# when our thread completes its target function (task),
# the supplied message is added to the queue
msg = MyQueue.get(0)
# check message
print(msg)
except queue.Empty:
# .get failed, come back in 100 and check again
print('no message')
threading.Timer(0.001, lambda q=MyQueue: process_queue(q)).start()
class ThreadedTask(threading.Thread):
"""threaded task handler"""
def __init__(self, queue, target, msg):
threading.Thread.__init__(self)
# message to add to queue when the task (target function) completes
self.msg = msg
# function to run
self._target = target
# queue to store completion message
self.queue = queue
def run(self):
"""called when object is instantiated"""
# start users task
try:
self._target()
except Exception as e:
self.queue.put('Thread Fail')
return
# we are done, pass the completion message
self.queue.put(self.msg)
if __name__ == '__main__':
MyQueue = queue.Queue()
MyThread = ThreadedTask(MyQueue, run_device, 'Thread Task: Run')
MyThread.start()
process_queue(MyQueue)

Python 3, using tkinter, trying to create a game where the user has to click a button as many times as they can until the time runs out

Basically, I'm trying to make a small game of sorts where the user has to click a button as many times as they can before the time runs out (5 seconds).
After 5 seconds have passed, the button is greyed/disabled. However, my code has trouble working, because the 'timer' works only when the user clicks the button. I want the timer to run regardless of whether the user has clicked the button.
So basically, when the program is run, even if the user hasn't clicked the button and 5 seconds have passed, the button should be greyed/disabled.
Here is my code:
from tkinter import *
import time
class GUI(Frame):
def __init__(self, master):
Frame.__init__(self,master)
self.result = 0
self.grid()
self.buttonClicks = 0
self.create_widgets()
def countTime(self):
self.end = time.time()
self.result =self.end - self.start
return (self.result)
def create_widgets(self):
self.start = time.time()
self.button1 = Button(self)
self.label = Label(self, text=str(round(self.countTime(),1)))
self.label.grid()
self.button1["text"] = "Total clicks: 0"
self.button1["command"] = self.update_count
self.button1.grid()
def update_count(self):
if(self.countTime() >=5):
self.button1.configure(state=DISABLED, background='cadetblue')
else:
self.buttonClicks+=1
self.button1["text"] = "Total clicks: " + str(self.buttonClicks)
root = Tk()
root.title("Something")
root.geometry("300x300")
app = GUI(root)
root.mainloop()
You must create a timer using the after()
from tkinter import *
import time
class GUI(Frame):
def __init__(self, master):
Frame.__init__(self,master)
self.result = 0
self.grid()
self.buttonClicks = 0
self.create_widgets()
self.isRunning = True
self.update_clock()
self.master = master
def countTime(self):
self.end = time.time()
self.result =self.end - self.start
return self.result
def create_widgets(self):
self.start = time.time()
self.button1 = Button(self)
self.label = Label(self, text=str(round(self.countTime(),1)))
self.label.grid()
self.button1["text"] = "Total clicks: 0"
self.button1["command"] = self.update_count
self.button1.grid()
def update_count(self):
if self.isRunning:
self.buttonClicks+=1
self.button1["text"] = "Total clicks: " + str(self.buttonClicks)
def update_clock(self):
t = round(self.countTime(), 1)
self.label.configure(text=str(t))
if t < 5:
self.master.after(100, self.update_clock)
else:
self.isRunning = False
root = Tk()
root.title("Something")
root.geometry("300x300")
app = GUI(root)
root.mainloop()
You should be running a different thread (As seen here: Tkinter: How to use threads to preventing main event loop from "freezing") to be running the timer.
Check this other question to get an idea on how your other thread should be like How to create a timer using tkinter?

Resources