Bottom line: pump the events with the below code after an action that causes a UI event, before a later action that needs the effect of that event.
IPython provides an elegant solution without threads it its gui tk
magic command implementation that's located in terminal/pt_inputhooks/tk.py
.
Instead of root.mainloop()
, it runs root.dooneevent()
in a loop, checking for exit condition (an interactive input arriving) each iteration. This way, the even loop doesn't run when IPython is busy processing a command.
With tests, there's no external event to wait for, and the test is always "busy", so one has to manually (or semi-automatically) run the loop at "appropriate moments". What are they?
Testing shows that without an event loop, one can change the widgets directly (with <widget>.tk.call()
and anything that wraps it), but event handlers never fire. So, the loop needs to be run whenever an event happens and we need its effect -- i.e. after any operation that changes something, before an operation that needs the result of the change.
The code, derived from the aforementioned IPython procedure, would be:
def pump_events(root):
while root.dooneevent(_tkinter.ALL_EVENTS|_tkinter.DONT_WAIT):
pass
That would process (execute handlers for) all pending events, and all events that would directly result from those.
(tkinter.Tk.dooneevent()
delegates to Tcl_DoOneEvent()
.)
As a side note, using this instead:
root.update()
root.update_idletasks()
would not necessarily do the same because neither function processes all kinds of events. Since every handler may generate other arbitrary events, this way, I can't be sure that I've processed everything.
Here's an example that tests a simple popup dialog for editing a string value:
class TKinterTestCase(unittest.TestCase):
"""These methods are going to be the same for every GUI test,
so refactored them into a separate class
"""
def setUp(self):
self.root=tkinter.Tk()
self.pump_events()
def tearDown(self):
if self.root:
self.root.destroy()
self.pump_events()
def pump_events(self):
while self.root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
pass
class TestViewAskText(TKinterTestCase):
def test_enter(self):
v = View_AskText(self.root,value=u"йцу")
self.pump_events()
v.e.focus_set()
v.e.insert(tkinter.END,u'кен')
v.e.event_generate('<Return>')
self.pump_events()
self.assertRaises(tkinter.TclError, lambda: v.top.winfo_viewable())
self.assertEqual(v.value,u'йцукен')
# ###########################################################
# The class being tested (normally, it's in a separate module
# and imported at the start of the test's file)
# ###########################################################
class View_AskText(object):
def __init__(self, master, value=u""):
self.value=None
top = self.top = tkinter.Toplevel(master)
top.grab_set()
self.l = ttk.Label(top, text=u"Value:")
self.l.pack()
self.e = ttk.Entry(top)
self.e.pack()
self.b = ttk.Button(top, text='Ok', command=self.save)
self.b.pack()
if value: self.e.insert(0,value)
self.e.focus_set()
top.bind('<Return>', self.save)
def save(self, *_):
self.value = self.e.get()
self.top.destroy()
if __name__ == '__main__':
import unittest
unittest.main()