Yet another question about tkinter terminating...
Here is a minimal code :
import threading,tkinter,random
class MiniTk :
done = lambda self : self._quit.is_set()
spot = lambda self,x,y : self._canv.create_oval( x-5,y-5,x+5,y+5 )
def __init__ (self,sx,sy):
self._sync = threading.Event()
self._quit = threading.Event()
def _thread ():
self._root = tkinter.Tk()
self._canv = tkinter.Canvas( self._root,width=sx,height=sy )
self._canv.pack()
self._root.bind('<Escape>',lambda e : self._root.quit())
self._sync.set()
self._root.mainloop()
self._root.destroy()
self._quit.set()
print('exit loop')
threading.Thread( target=_thread ).start()
self._sync.wait()
tk = MiniTk( 800,600 )
while not tk.done():
tk.spot( random.randint( 0,800 ),random.randint( 0,600 ))
print('done')
It draws spots until escape is pressed.
But when I do so, it prints "exit loop" as expected,
the GUI is well closed, but it never prints "done" and the program never terminate.
The thread seems lost somewhere...
Is there a good practice I missed ?
None of the answers so far explain why your original script didn't work. Tkinter, in its latest versions, supports multithreading. Provided good programming practices are used, it is possible to update the GUI from multiple threads. Elaborate mechanisms, such as queues to serialize access into a single thread, aren't strictly necessary.
In your original script, the main thread draws circles as fast as it can, while the secondary thread waits for events. That isn't a problem. However, when the secondary thread closes the GUI, that will delete the objects that the main thread is using when it draws. This is where the problem occurs. Updating the GUI from two threads is fine as long as you don't try to do two things at once that are logically contradictory.
Tkinter is a wrapper around the Tcl GUI engine, so there are two categories of objects in play here: Tcl objects, and Python objects. It is possible for the Tcl object to be deleted while the corresponding Python object remains alive. The while loop in the main thread will spend the overwhelming majority of its time in the loop body, rather than in the while statement itself. So it is almost almost always in the process of drawing circles, rather than checking the status of the done flag.
To fix this problem, all you need to do is to enforce the proper sequencing of events at shutdown. You first need to set the done flag, in your event handler. Then you need to close the GUI after you are done drawing circles. The simplest way to do this is to have the main loop close the GUI.
The following script works, and is nothing more than a reorganization of your code.
[I have taken the liberty of changing your lambda statements into member functions. Ordinary member functions ought to look like ordinary member functions, IMO.]
import threading
import tkinter
import random
class MiniTk :
def __init__ (self, sx, sy):
self._sync = threading.Event()
self._quit = threading.Event()
def loop ():
self._root = tkinter.Tk()
self._canv = tkinter.Canvas( self._root,width=sx,height=sy )
self._canv.pack()
self._root.bind('<Escape>', self.escape)
self._sync.set()
self._root.mainloop()
print('exit loop')
threading.Thread( target=loop ).start()
self._sync.wait()
def done(self):
return self._quit.is_set()
def spot(self, x, y):
self._canv.create_oval(x-5, y-5, x+5, y+5)
def escape(self, _):
self._quit.set()
def quit(self):
self._root.quit()
tk = MiniTk(800, 600)
while not tk.done():
tk.spot( random.randint( 0,800 ),random.randint( 0,600 ))
tk.quit()
print('done')
I realize that this isn't your actual program, but I'm hoping that this suggests a usable program design.