Trying To Fix Tkinter Gui Freeze-ups (using Threads)
Solution 1:
You'll need two functions: the first encapsulates your program's long-running work, and the second creates a thread that handles the first function. If you need the thread to stop immediately if the user closes the program while the thread is still running (not recommended), use the daemon
flag or look into Event
objects. If you don't want the user to be able to call the function again before it's finished, disable the button when it starts and then set the button back to normal at the end.
import threading
import tkinter as tk
import time
classApp:
def__init__(self, parent):
self.button = tk.Button(parent, text='init', command=self.begin)
self.button.pack()
deffunc(self):
'''long-running work'''
self.button.config(text='func')
time.sleep(1)
self.button.config(text='continue')
time.sleep(1)
self.button.config(text='done')
self.button.config(state=tk.NORMAL)
defbegin(self):
'''start a thread and connect it to func'''
self.button.config(state=tk.DISABLED)
threading.Thread(target=self.func, daemon=True).start()
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.mainloop()
Solution 2:
I've modified the question to include the accidentally omitted but critical line. The answer to avoiding GUI freezeups turns out to be embarrassingly simple:
Don't call ".join()" after launching the thread.
In addition to the above, a complete solution involves:
- Disabling the "Do Reports" button until the "create report" thread finishes (technically not necessary but preventing extra report creation threads also prevents user confusion);
- Having the "create report" thread update the main thread using these events:
- "Completed report X" (an enhancement that displays progress on GUI), and
- "Completed all reports" (display the "Done" window and reenable the "Do Reports" button);
- Moving the invocation of the "Done" window to the main thread, invoked by the above event; and
- Passing data with the event instead of using shared global variables.
A simple approach using the multiprocessing.dummy module (available since 3.0 and 2.6) is:
from multiprocessing.dummy importProcessReportCreationProcess= Process( target = DoChosenReports )
ReportCreationProcess.start()
again, note the absence of a .join() line.
As a temporary hack the "Done" window is still being created by the create report thread just before it exits. That works but does cause this runtime error:
RuntimeError: Calling Tcl from different appartment
however the error doesn't seem to cause problems. And, as other questions have pointed out, the error can be eliminated by moving the creation of the "DONE" window into the main thread (and have the create reports thread send an event to "kick off" that window).
Finally my thanks to @TigerhawkT3 (who posted a good overview of the approach I'm taking) and @martineau who covered how to handle the more general case and included a reference to what looks like a useful resource. Both answers are worth reading.
Solution 3:
I found a good example similar to what you want to do in one of the books I have which I think shows a good way of using threads with tkinter. It's Recipe 9.6 for Combining Tkinter and Asynchronous I/O with Threads in the first edition of the book Python Cookbook by Alex Martinelli and David Ascher. The code was written for Python 2.x, but required only minor modifications to work in Python 3.
As I said in a comment, you need to keep the GUI eventloop running if you want to be able to interact with it or just to resize or move the window. The sample code below does this by using a Queue
to pass data from the background processing thread to the main GUI thread.
Tkinter has a universal function called after()
which can be used schedule a function to be called after certain amount time has passed. In the code below there's a method named periodic_call()
which processes any data in the queue and then calls after()
to schedule another call to itself after a short delay so the queue data processing will continue.
Since after()
is part of tkinter, it allows the mainloop()
to continue running which keeps the GUI "alive" between these periodic queue checks. It can also make tkinter
calls to update the GUI if desired (unlike code that's running in separate threads).
from itertools import count
import sys
import tkinter as tk
import tkinter.messagebox as tkMessageBox
import threading
import time
from random import randint
import queue
# Based on example Dialog # http://effbot.org/tkinterbook/tkinter-dialog-windows.htmclassInfoMessage(tk.Toplevel):
def__init__(self, parent, info, title=None, modal=True):
tk.Toplevel.__init__(self, parent)
self.transient(parent)
if title:
self.title(title)
self.parent = parent
body = tk.Frame(self)
self.initial_focus = self.body(body, info)
body.pack(padx=5, pady=5)
self.buttonbox()
if modal:
self.grab_set()
ifnot self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.geometry("+%d+%d" % (parent.winfo_rootx()+50, parent.winfo_rooty()+50))
self.initial_focus.focus_set()
if modal:
self.wait_window(self) # Wait until this window is destroyed.defbody(self, parent, info):
label = tk.Label(parent, text=info)
label.pack()
return label # Initial focus.defbuttonbox(self):
box = tk.Frame(self)
w = tk.Button(box, text="OK", width=10, command=self.ok, default=tk.ACTIVE)
w.pack(side=tk.LEFT, padx=5, pady=5)
self.bind("<Return>", self.ok)
box.pack()
defok(self, event=None):
self.withdraw()
self.update_idletasks()
self.cancel()
defcancel(self, event=None):
# Put focus back to the parent window.
self.parent.focus_set()
self.destroy()
classGuiPart:
TIME_INTERVAL = 0.1def__init__(self, master, queue, end_command):
self.queue = queue
self.master = master
console = tk.Button(master, text='Done', command=end_command)
console.pack(expand=True)
self.update_gui() # Start periodic GUI updating.defupdate_gui(self):
try:
self.master.update_idletasks()
threading.Timer(self.TIME_INTERVAL, self.update_gui).start()
except RuntimeError: # mainloop no longer running.passdefprocess_incoming(self):
""" Handle all messages currently in the queue. """while self.queue.qsize():
try:
info = self.queue.get_nowait()
InfoMessage(self.master, info, "Status", modal=False)
except queue.Empty: # Shouldn't happen.passclassThreadedClient:
""" Launch the main part of the GUI and the worker thread. periodic_call()
and end_application() could reside in the GUI part, but putting them
here means all the thread controls are in a single place.
"""def__init__(self, master):
self.master = master
self.count = count(start=1)
self.queue = queue.Queue()
# Set up the GUI part.
self.gui = GuiPart(master, self.queue, self.end_application)
# Set up the background processing thread.
self.running = True
self.thread = threading.Thread(target=self.workerthread)
self.thread.start()
# Start periodic checking of the queue.
self.periodic_call(200) # Every 200 ms.defperiodic_call(self, delay):
""" Every delay ms process everything new in the queue. """
self.gui.process_incoming()
ifnot self.running:
sys.exit(1)
self.master.after(delay, self.periodic_call, delay)
# Runs in separate thread - NO tkinter calls allowed.defworkerthread(self):
while self.running:
time.sleep(randint(1, 10)) # Time-consuming processing.
count = next(self.count)
info = 'Report #{} created'.format(count)
self.queue.put(info)
defend_application(self):
self.running = False# Stop queue checking.
self.master.quit()
if __name__ == '__main__': # Needed to support multiprocessing.
root = tk.Tk()
root.title('Report Generator')
root.minsize(300, 100)
client = ThreadedClient(root)
root.mainloop() # Display application window and start tkinter event loop.
Post a Comment for "Trying To Fix Tkinter Gui Freeze-ups (using Threads)"