pythontkinterpython-3.14

Alt, Shift and Control key bindings in Tkinter


I am struggling with some key bindings in Tkinter.

In my application, to avoid having to make each widget get the focus prior to having the opportunity of capturing KeyPress events, I decided to use root.bind_all() to let the root window capture all those events. I want to be able to differentiate between, for example, "a", "Alt+a", "Shift+a" and "Control+a".

Below is a simple program where I have 4 different blocks of code, the tests I have done and the results I have got:

import tkinter as tk
from tkinter import ttk

class App:
    def __init__(self, root):
        self.root = root
        label = ttk.Label(self.root, text="Key binding test", takefocus=True)
        label.pack()        
        
        # BLOCK 1: to test this block, uncomment it and comment the rest of blocks
        self.root.bind_all("<Key>", self.change_color)
        
        # My results:
        #   Test 1.1: Press 'a' -> 'Event captured:  a'
        #   Test 1.2: Press Shift -> 'Event captured:  Shift_L'
        #   Test 1.3: Press Shift+a -> 'Event captured:  Shift_L'
        #                              'Event captured:  A'
        #   Test 1.4: Press Alt -> no event captured
        #   Test 1.5: Press Alt+a -> no event captured
        #   Test 1.6: Press Control -> 'Event captured:  Control_L'
        #   Test 1.7: Press Control+a -> 'Event captured:  Control_L'
        #                                'Event captured:  a'
        #   Test 1.8: Press Alt gr -> 'Event captured:  Control_L'
        
        
        # BLOCK 2: to test this block, uncomment it and comment the rest of blocks
        #self.root.event_add("<<KEYS_1>>", 
        #    "<Shift-a>",
        #    "<Alt-a>",
        #    "<Control-a>"
        #    )
        #self.root.bind_all("<<KEYS_1>>", self.change_color)
        
        # My results:
        #   Test 2.1: Press Shift -> no event captured
        #   Test 2.2: Press Shift+a -> no event captured
        #   Test 2.3: Press Alt -> no event captured
        #   Test 2.4: Press Alt+a -> 'Event captured:  a'
        #   Test 2.5: Press Control -> no event captured
        #   Test 2.6: Press Control+a -> 'Event captured:  a'
        

        # BLOCK 3: to test this block, uncomment it and comment the rest of blocks
        #self.root.event_add("<<KEYS_2>>", 
        #    "<Shift_L>",
        #    "<Alt_L>",
        #    "<Control_L>"
        #    )
        #self.root.bind_all("<<KEYS_2>>", self.change_color)

        # My results:
        #   Test 3.1: Press Shift -> 'Event captured:  Shift_L'
        #   Test 3.2: Press Alt -> no event captured
        #   Test 3.3: Press Control -> 'Event captured:  Control_L'

        
        # BLOCK 4: to test this block, uncomment it and comment the rest of blocks
        #self.root.bind_all("<Alt_L>", self.change_color)
        #self.root.bind_all("<Shift_L>", self.change_color)
        #self.root.bind_all("<Control_L>", self.change_color)
        
        # My results:
        #   Test 4.1: Press Alt -> 'Event captured:  Alt_L'
        #   Test 4.2: Press Shift -> 'Event captured:  Shift_L'
        #   Test 4.3: Press Control -> 'Event captured:  Control_L'
        
        
    def change_color(self, event):
        # Ideally do the change color stuff
        print('Event captured: ', event.keysym)
        
        # Also tested with
        #   return "break"
        # and
        #   return ("break")
        # and 
        #   return


if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

Test 1 is about capturing any key ("<Key>" binding), Test 2 about Shift, Alt and Control modifiers for Key-a, Test 3 for Shift_L, Alt_L and Control_L key names. Test 4 should be equal to Test 3.

As can be seen,

Test 1.2 captured Shift_L but Test 1.4 didn't capture Alt_L.

Tests 2.1 and 2.2 didn't capture Shift being pressed

Tests 2.4 and 2.6 just printed 'a', meaning I wouldn't know if Alt or Control were used as modifiers first

Test 3.2 didn't capture Alt_L, but Test 4.1 did! And Test 1.4 didn't!

I have tried changing

            "<Shift-a>",
            "<Alt-a>",
            "<Control-a>"

for

            "<Shift-KeyPress-a>",
            "<Alt-KeyPress-a>",
            "<Control-KeyPress-a>"

as suggested by TheLizzard here

"Alt" bindings not working in tkinter text widget

but I got the same results.

Any ideas for this "strange behaviour"? Can it be OS dependant?

I am using Python 3.14 on Windows 11.

I have tested it on a Dell laptop, and also on a HP laptop with external keyboard connected by USB port.

The only way I have found to make it work is to use Symon's approach here:

Binding buttons to Alt keypresses?

defininig his suggested AltOn and AltOff functions, and creating new ShiftOn, ShiftOff, ControlOn and ControlOff ones.


Solution

  • Here's a minimal working example with event bindings for a, Shift+a, Ctrl+a, and Alt+a (naturally these bindings can be modified to use other keys as needed)

    import tkinter as tk
    
    
    class App(tk.Tk):
        def __init__(self) -> None:
            super().__init__()
            self.title('App')
            self.lbl = tk.Label(self, text='Press a, ctrl+a, alt+a, or shift+a')
            self.lbl.pack()
    
            self.bind('a', lambda e: self.lbl.config(text=e))  # just 'a'
            self.bind('<Control-a>', lambda e: self.lbl.config(text=e))
            self.bind('<Alt-a>', lambda e: self.lbl.config(text=e))
            self.bind('A', lambda e: self.lbl.config(text=e))  # Shift + a
    
    
    if __name__ == '__main__':
        app = App()
        app.mainloop()
    

    The only one that may be a bit unintuitive is Shift+a, which you'll notice is actually bound to capital A!

    Pressing any of these key bindings will show their event info in the Label widget.

    You can look at the event's state attribute (i.e., e.state in this example) to see what modifier keys, if any, were held when the binding was triggered.

    I've used bind here, but bind_all would also work for this example.


    NB: My App class inherits directly from tk.Tk, so I don't have a root here as it isn't necessary