I am trying to figure out how to unittest a bind command in a dialog window. I'm attempting this with tkinter's event_generate
. It is not working the way I expect. For this StackOverflow question I've set up some code with a single call to event_generate
. Sometimes that line works and sometimes it is as if the line doesn't even exist.
The bind in the dialog's __init__
method looks like this:
self.bind('<BackSpace>', #Print "BackSpace event generated."
lambda event: print(event.keysym, 'event generated.'))
Any action in the dialog will call back to its terminate method (The dialog is based on Frederik Lundh's example Dialog in 'An Introduction to Tkinter'.)
def terminate(self, event=None):
print('terminate called') # Make sure we got here and the next line will be called
self.event_generate('<BackSpace>')
self.parent.focus_set()
self.destroy()
When the dialog is called using the code below any user action will end up calling terminate
. In each case "terminate called" and "BackSpace event generated." are displayed. This proves that the call to event_generate is set up correctly.
parent = tk.Tk()
dialog = Dialog(parent)
dialog.wait_window()
In case it's relevant I ought to mention that I have moved Lundh's call to self.wait_window
from his dialog's __init__
method to the caller. Whilst this breaks the neat encapsulation of his dialog it appears to be necessary for automated unittests. Otherwise the unittest will display the dialog and halt waiting for user input. I don't like this solution but I'm not aware of any alternative.
The problem I'm having is when wait_window
is replaced with a direct call to the terminate
method. This is the sort of thing that I'd expect to be able to do in unittesting which is to test my GUI code without running tkinter's mainloop or wait_window.
parent = tk.Tk()
dialog = Dialog(parent)
dialog.terminate()
This only prints "terminate called" and does not print "BackSpace event generated.". The call to event_generate
appears to have no effect. If I follow the call in the debugger I can see that tkinter's event_generate()
is being called with the correct arguments. self = {Dialog} .99999999, sequence = {str}'<BackSpace>', kw = {dict}{}
In view of the warning in the TkCmd man pages about window focus I have verified the dialog with the binding is given focus in its __init__
method.
Tkinter is not executing the callback. Why?
EDIT: This bare bones code shows update
working. However, it only works if it is called in __init__
before event_generate
is called by the main program. (This puzzle has been raised as a separate question)
class UpdWin(tk.Tk):
def __init__(self):
super().__init__()
self.bind('<BackSpace>',
lambda event: print(event.keysym, 'event generated.'))
self.update() # Update works if placed here
app = UpdWin()
app.event_generate('<BackSpace>')
# app.update() # Update doesn't work if placed here
4/12/2021. See Mark Roseman's excellent web site for a detailed explanation of why any use of update
is a bad idea.
The problem posed by this six year old question is entirely avoided by better program design in which tkinter widget objects are never subclassed. Instead they should be created by composition where they can be easily monkey patched. (This advice is contrary to patterns shown in Frederik Lundh's example Dialog in 'An Introduction to Tkinter'.) For unittest design, not only is there no need to start Tk/Tcl via tkinter but it is also unwise.
event_generate
will by default process all event callbacks immediately. However, if you don't call update before calling event_generate
, the window won't be visible and tkinter will likely ignore any events. You can control when the generated event is processed with the when
attribute. By default the value is "now", but another choice is "tail" which means to append it to the event queue after any events (such as redraws) have been processed.
Full documentation on the when
attribute is on the tcl/tk man page for event_generate
: http://tcl.tk/man/tcl8.5/TkCmd/event.htm#M34
Don't know if this is relevant to your problem, but I got widget.event_generate()
to work by calling widget.focus_set()
first.
@lemi57ssss I know this is an old question, but I just want to highlight the point brought up by Bryan Oakley and to correct your last code to make it work. He said you have to update first before it can respond to the generated event. So if you switch the positions of update()
and event_generate()
, you will get the "BackSpace event generated." text printed out.
It worked when you put the update()
in the __init__()
was because of the same reason, i.e., it got called first before the event_generated()
.
See the amended code below:
class UpdWin(tk.Tk):
def __init__(self):
super().__init__()
self.bind('<BackSpace>',
lambda event: print(event.keysym, 'event generated.'))
#self.update() # Update works if placed here
app = UpdWin()
app.update() # Update also works if you placed it here
app.event_generate('<BackSpace>')
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With