Issue
I have a Python 3.11.5 program that uses a Tkinter GUI to receive user input, and runs a number of processes in the background. I'm wondering what the preferred method is for handling when the user wants to cancel a background process while it's still running. I've tried to make cancel buttons, to use separate threads and threading.Event
s to signal a cancel, but I can't get it to exit cleanly. I'd like to be able to hit cancel, take a couple seconds if needed, but see the program end and my terminal go back to normal, with no errors.
Here's a minimum example:
import tkinter as tk
import time
class app(tk.Tk):
def __init__(self):
super().__init__()
self.title("An app")
self.create_widgets()
self.a_process()
def create_widgets(self):
self.text = tk.Label(self, text = "Hello World!")
self.text.pack()
self.cancelButton = tk.Button(self, text = "Cancel", command = self.destroy)
self.cancelButton.pack()
def a_process(self):
for i in range(100):
self.text.config(text = f"Message # {i}")
self.text.update()
time.sleep(1)
def destroy(self):
super().destroy()
if __name__ == "__main__":
root = app()
root.mainloop()
Hitting cancel while the message loop is running gives this error:
Traceback (most recent call last):
File "C:\Users\M.Modeler\geoSieve 1.1\test.py", line 27, in <module>
root = app()
^^^^^
File "C:\Users\M.Modeler\geoSieve 1.1\test.py", line 9, in __init__
self.a_process()
File "C:\Users\M.Modeler\geoSieve 1.1\test.py", line 19, in a_process
self.text.config(text = f"Message # {i}")
File "C:\Users\M.Modeler\AppData\Local\miniconda3\envs\geoSieve\Lib\tkinter\__init__.py", line 1702, in configure
return self._configure('configure', cnf, kw)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\M.Modeler\AppData\Local\miniconda3\envs\geoSieve\Lib\tkinter\__init__.py", line 1692, in _configure
self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
_tkinter.TclError: invalid command name ".!label"
What is my destroy
method missing to end the background process and exit cleanly?
Solution
you shouldn't be using a blocking infinite loop in your GUI, instead use self.after
to schedule a function to be called later, now you don't need to call update
to redraw the GUI, and as a side-bonus when the GUI exits the function won't be called anymore.
import tkinter as tk
import time
class app(tk.Tk):
def __init__(self):
super().__init__()
self.title("An app")
self.create_widgets()
self.after(1000, lambda: self.a_process(0))
def create_widgets(self):
self.text = tk.Label(self, text = "Hello World!")
self.text.pack()
self.cancelButton = tk.Button(self, text = "Cancel", command = self.destroy)
self.cancelButton.pack()
def a_process(self, i):
self.text.config(text = f"Message # {i}")
if i+1 < 100: # call this function later with different args
self.after(1000,lambda: self.a_process(i+1))
def destroy(self):
super().destroy()
if __name__ == "__main__":
root = app()
root.mainloop()
alternatively if you have a long task that you want to run in the background you could schedule it on a threading.Thread
that is daemon (will terminate when python exits), and have it call self.after
on a function that updates the GUI.
as functions called with after
won't happen when your mainloop ends, you won't be trying to update the GUI when the GUI exits.
import tkinter as tk
import time
import threading
class app(tk.Tk):
def __init__(self):
super().__init__()
self.title("An app")
self.create_widgets()
self.counter = 0
thread = threading.Thread(target = self.a_process, args=[], daemon=True)
thread.start()
def create_widgets(self):
self.text = tk.Label(self, text = "Hello World!")
self.text.pack()
self.cancelButton = tk.Button(self, text = "Cancel", command = self.destroy)
self.cancelButton.pack()
def a_process(self):
for i in range(100):
self.counter += 1
self.after(0,self.update_counter)
time.sleep(1)
def update_counter(self):
self.text.config(text = f"Message # {self.counter}")
def destroy(self):
super().destroy()
if __name__ == "__main__":
root = app()
root.mainloop()
Answered By - Ahmed AEK
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.