Search code examples
pythontkinterframekey-bindings

Tkinter - key-binding to frame won't work if a child widget gets focus


I'm trying to write a program with different key-binds for different frames.

This works fine if the frame is put into focus, but if another widget is put into focus, the key-bind does not work.

For example, in this trial code, the key bind will work if no widget is in focus, but if the button is put into focus (either programatically or through using the TAB wkey to focus the button), then the key bind on the frame no longer works. Setting the focus on the frame, after setting it on the button widget does not help.

import tkinter as tk


class TestGUI(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)

        self.edit_text = tk.StringVar()

        self.intro_label = tk.Label(self, text="Press button to say Hello")
        self.hello_button = tk.Button(self, text='Say Hello (H)', command=self.press_button)
        self.text_area = tk.Label(self, textvariable=self.edit_text, width=20, height=5)

        self.intro_label.pack(padx=10, pady=5)
        self.hello_button.pack(pady=5)
        self.text_area.pack(padx=10, pady=(5, 10))

        self.bind('<Key>', self.press_key)
        self.hello_button.focus()
        self.focus()

    def press_button(self):
        txt = self.edit_text.get()
        txt += "Hello World!\n"
        self.edit_text.set(txt)

    def press_key(self, event):
        key_pressed = event.char.lower()
        if key_pressed == "h":
            self.hello_button.invoke()


if __name__ == "__main__":
    root = tk.Tk()
    TestGUI(root).pack()
    root.mainloop()

I know that the key bind will still work if I put the bind on the application level:

parent.bind('<Key>', self.press_key)

However, I want to use different key binds on different frames in the application. Is there a way to do this, without losing the key bind if another widget gains focus?


Solution

  • Sorry, no, that's not a feature that tkinter has. Binding to a window is a special case that allows any child in the window react to the bind.

    It sounds like you want to use a different key for every frame? So the first frame responds to 'h', and the second to 's' or something? In that case you can simply bind each frame to exactly what you want, instead of all keys. This also avoids the need for a second method to invoke the button press:

    import tkinter as tk
    
    class TestGUI(tk.Frame):
        def __init__(self, parent):
            super().__init__(parent)
    
            self.edit_text = tk.StringVar()
    
            self.intro_label = tk.Label(self, text="Press button to say Hello")
            self.hello_button = tk.Button(self, text='Say Hello (H)', command=self.press_button)
            self.text_area = tk.Label(self, textvariable=self.edit_text, width=20, height=5)
    
            self.intro_label.pack(padx=10, pady=5)
            self.hello_button.pack(pady=5)
            self.text_area.pack(padx=10, pady=(5, 10))
    
            self.bind_all('<h>', self.press_button) # bind to the 'h' key
            # ~ parent.bind('<h>', self.press_button) # alternative way
            self.hello_button.focus()
            self.focus()
    
        def press_button(self, event=None):
            txt = self.edit_text.get()
            txt += "Hello World!\n"
            self.edit_text.set(txt)
    
    if __name__ == "__main__":
        root = tk.Tk()
        TestGUI(root).pack()
        root.mainloop()
    

    Otherwise I suppose you could make a quick loop to bind to all the children in a frame.

    def set_child_bind(widget, event, callback):
        widget.bind(event, callback)
        for child in widget.children.values():
            set_child_bind(child, event, callback)
    
    class TestGUI(tk.Frame):
        def __init__(self, parent):
            super().__init__(parent)
            # all the other stuff
            set_child_bind(self, '<Key>', self.press_key)
    

    Or you could use the FocusIn event to grab application-wide binding:

    class TestGUI(tk.Frame):
        def __init__(self, parent):
            super().__init__(parent)
            # all the other stuff
            self.bind('<FocusIn>', lambda e:self.bind_all('<Key>', self.press_key))
            self.bind('<FocusOut>', lambda e:self.unbind_all('<Key>'))