Итог: добавьте события с кодом ниже после действия, которое вызывает событие пользовательского интерфейса, до более позднего действия, которое требует эффекта этого события.
IPython предоставляет элегантное решение без потоков, это gui tk
магическая реализация команды, расположенная в terminal/pt_inputhooks/tk.py
.
Вместо root.mainloop()
он запускает root.dooneevent()
в цикле, проверяя условие выхода (поступает интерактивный ввод) на каждой итерации. Таким образом, четный цикл не запускается, когда IPython занят обработкой команды.
В тестах нет внешнего события, которое нужно ждать, и тест всегда «занят», поэтому необходимо вручную (или полуавтоматически) запускать цикл в «подходящие моменты». Что они?
Тестирование показывает, что без цикла событий можно напрямую изменять виджеты (с помощью <widget>.tk.call()
и всего, что с ним связано), но обработчики событий никогда не запускаются. Таким образом, цикл должен выполняться всякий раз, когда происходит событие, и нам нужен его эффект - то есть после любой операции, которая что-то меняет, перед операцией, которая нуждается в результате изменения.
Код, полученный из вышеупомянутой процедуры IPython, будет:
def pump_events(root):
while root.dooneevent(_tkinter.ALL_EVENTS|_tkinter.DONT_WAIT):
pass
Это обработало бы (выполнило обработчики) все ожидающие события и все события, которые непосредственно были бы результатом этих событий.
(tkinter.Tk.dooneevent()
делегатов на Tcl_DoOneEvent()
.)
В качестве примечания используйте вместо этого:
root.update()
root.update_idletasks()
не обязательно будет делать то же самое, потому что ни одна из функций не обрабатывает все виды событий. Поскольку каждый обработчик может генерировать другие произвольные события, таким образом, я не могу быть уверен, что все обработал.
Вот пример, который тестирует простое всплывающее диалоговое окно для редактирования строкового значения:
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"йцу") # the class implementing the dialog;
# not included in the example
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()