2

EDIT: Possible solution below, can anyone confirm?

I'm running tkinter in a thread, and attempting to use the event_generate technique described in this answer to control its behaviour from other threads. Clearly, once I have started a thread which sets up a tkinter.Tk instance and starts its mainloop, it is necessary to block until the mainloop has started before attempting to generate events. The way I was trying to do this is as follows (python 3.2 code):

import tkinter
import threading

mainloop_started = threading.Event()

def tkinter_thread_stuff():
    global root
    root = tkinter.Tk()

    #[code that binds events with root.bind]

    root.after(0, mainloop_started.set)
    root.mainloop()

th = threading.Thread(target=tkinter_thread_stuff)
th.start()

mainloop_started.wait()

#[code that sends events to root with event_generate]

In other words, I am using after to cause the tkinter mainloop to set the threading.Event called mainloop_started, and my event-generating code starts by blocking until this event is set. However, this seems to lead to a race condition - sometimes events generated quickly after mainloop_started.wait() are never processed by tkinter. If I put a time delay in the after call (e.g. root.after(1000, mainloop_started.set)), this does not happen, so it looks like there is some time in between the point where tkinter calls the after callback, and the point at which it is able to receive events. Does anyone know the correct way of blocking until tkinter can receive events?

PS: is it safe to run the tkinter mainloop inside a thread like this? I know I can't interfere directly with root or other tkinter stuff from other threads, but apart from that, everything seems to work OK. I have seen some sites that say tkinter can only be run in the main thread.

EDIT: I think I have a solution - use after_idle instead of after. This seems to make sure that the callback isn't called until the mainloop is ready to process events, but can anyone confirm this? Anyway, what is the point of after if you can't guarantee that tkinter will be fully set up when the callback is called (unless after is called in another event handler, I suppose)?

A concrete example of the effects of using after and after_idle in this case if anyone wants to play around with it:

import tkinter
import threading

#create a threading.Event for blocking until the mainloop is ready
mainloop_started = threading.Event()

#counter stores the number of times that tkinter receives the event
#<<increment>>, which is generated 1000 times
counter = 0

#delay in milliseconds before mainloop sets mainloop_started:
#if I set this to 0, the final value of counter varies between 0 and 1000, i.e.
#some of the <<increment>> events may not be processed as expected
#if I set it to 1 or a larger number, the final value of counter is always 1000,
#so all events are processed correctly, and I can get the
#same behaviour by replacing after with after_idle below
delay_ms = 0

def send_events():
    global root

    #wait until mainloop_started has been set
    mainloop_started.wait()

    #send 1000 <<increment>> events
    for i in range(1000):
        root.event_generate('<<increment>>', when='tail')

    #send a <<show>> event
    root.event_generate('<<show>>', when='tail')

#run send_events in a thread
th = threading.Thread(target=send_events)
th.start()

#start tkinter
root = tkinter.Tk()

#when root receives the event <<increment>>, increment counter
def increment(event):
    global counter
    counter += 1
root.bind('<<increment>>', increment)

#when root receives the event <<show>>, print the value of the counter
def show(event):
    print(counter)
root.bind('<<show>>', show)

#instruct mainloop to set mainloop_started
root.after(delay_ms, mainloop_started.set)

#using after_idle instead causes all events to be processed correctly
#root.after_idle(mainloop_started.set)

#finally, start the mainloop
root.mainloop()
Community
  • 1
  • 1
James
  • 3,191
  • 1
  • 23
  • 39
  • I don't dare to run anything tkinter related in somewhere else than main thread, I tried but it crashed in one of my computers. Then I decided that event_generate in secondary thread is safe, but even this caused me trouble when I needed to create a Toplevel. "event_generate" inside "after_idle" seemed to work, but I wasn't sure anymore, so I changed my plans (used polling instead of events) – Aivar Jul 25 '13 at 17:06
  • @Aivar Your comment is way too **subjective**. Is it or is it not possible to call `.event_generate` from a thread? This is fear mongering. – WhyWhat Apr 14 '20 at 09:37

0 Answers0