This might be the most complicated question that I've asked on here. I've spent some time getting my code to the most simple that I think I can to reproduce my issue. I hope it's not too complicated to get any help...
Basically in the code below, a tkinter app with a single button is created, and it checks a queue every 100ms beccause a different thread may need to interact with it later. A new window is also created and destroyed very quickly because I get an error later otherwise (this may be important)
When the button is clicked, a new thread is made that tells the main thread (via the queue) to create a window that would be used to indicate something potentially time-consuming is happenning, then when that finishes, it tells the main thread (via the queue) to destroy the window.
The problem is that the window is not destroyed if the time consuming task is very short and there is no error either, but if the process in the thread took a long time (a second for example), it works as expected.
I wonder whether it is something like "The new window object hasn't been created and assigned to new_window
yet, so when I add the destroy method to the queue, I am actually adding the old (previously destroyed) object's destroy method to the queue". That would explain why I'm getting an error the first time I click the button if I don't create and destroy the window when I initialise the app, but it doesn't explain why I don't get an error calling destroy()
on a previously destroyed Toplevel
... If I'm right with that theory, I don't really know what the solution is, so any ideas would be appreciated
import tkinter as tk
import queue
import threading
import time
def button_pressed():
threading.Thread(target=do_something_on_a_thread).start()
def do_something_on_a_thread():
global new_window
app_queue.put(create_a_new_window)
time.sleep(1)
app_queue.put(new_window.destroy)
def create_a_new_window():
global new_window
new_window = tk.Toplevel()
tk.Label(new_window, text='Temporary Window').grid()
#Check queue and run any function that happens to be in the queue
def check_queue():
while not app_queue.empty():
queue_item = app_queue.get()
queue_item()
app.after(100, check_queue)
#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()
create_a_new_window()
new_window.destroy()
app.after(100, check_queue)
tk.mainloop()
My solution that appears to work is to use locks. I acquire a lock before I send the message to the queue that tells the main thread to create the toplevel. After the main thread has created the toplevel, it releases the lock.
Now before I send the message to destroy the toplevel it, I acquire the lock again which will block until the main thread has finished creating it.
import tkinter as tk
import queue
import threading
import time
def button_pressed():
threading.Thread(target=do_something_on_a_thread).start()
def do_something_on_a_thread():
global new_window
my_lock.acquire()
app_queue.put(create_a_new_window)
my_lock.acquire()
app_queue.put(new_window.destroy)
def create_a_new_window():
global new_window
new_window = tk.Toplevel()
tk.Label(new_window, text='Temporary Window').grid()
#Check queue and run any function that happens to be in the queue
def check_queue():
while not app_queue.empty():
queue_item = app_queue.get()
queue_item()
my_lock.release()
app.after(100, check_queue)
#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
my_lock = threading.Lock()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()
create_a_new_window()
new_window.destroy()
app.after(100, check_queue)
tk.mainloop()
Another (probably more simple) solution that I came up with was to create the window on the main thread after the button was pressed, which will prevent the thread from being started until the window has been created:
import tkinter as tk
import queue
import threading
import time
def button_pressed():
create_a_new_window()
threading.Thread(target=do_something_on_a_thread).start()
def do_something_on_a_thread():
global new_window
app_queue.put(new_window.destroy)
def create_a_new_window():
global new_window
new_window = tk.Toplevel()
tk.Label(new_window, text='Temporary Window').grid()
#Check queue and run any function that happens to be in the queue
def check_queue():
while not app_queue.empty():
queue_item = app_queue.get()
queue_item()
app.after(100, check_queue)
#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()
create_a_new_window()
new_window.destroy()
app.after(100, check_queue)
tk.mainloop()