Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tkinter's event_generate command ignored

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

Six Years On

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.

like image 874
lemi57ssss Avatar asked Dec 20 '14 15:12

lemi57ssss


3 Answers

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

like image 178
Bryan Oakley Avatar answered Nov 09 '22 18:11

Bryan Oakley


Don't know if this is relevant to your problem, but I got widget.event_generate() to work by calling widget.focus_set() first.

like image 38
Peter McKone Avatar answered Nov 09 '22 20:11

Peter McKone


@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>')
like image 1
kaosad Avatar answered Nov 09 '22 20:11

kaosad