Multiple Key Event Bindings in Tkinter - "Control + E" "Command (apple) + E" etc

This appears to be a bug in Tk. I get the same error with tcl/tk on the mac as well as with python/tkinter. You can bind <Command-e> to a widget (I tried with a text widget) but binding it to the root window or to "all" seems to cause the error you get.


Enhanced to cover the Alt and Meta keys, aka Option and Command on macOS.

# Original <https://StackOverflow.com/questions/6378556/
#           multiple-key-event-bindings-in-tkinter-control-e-command-apple-e-etc>

# Status of alt (ak option), control, meta (aka command)
# and shift keys in Python tkinter

# Note, tested only on macOS 10.13.6 with Python 3.7.4 and Tk 8.6.9

import tkinter as tk
import sys

_macOS = sys.platform == 'darwin'
_Alt   = 'Option' if _macOS else 'Alt'
_Ctrl  = 'Control'
_Meta  = 'Command' if _macOS else 'Meta'
_Shift = 'Shift'

alt = ctrl = meta = shift = ''


def up_down(mod, down):
    print('<%s> %s' % (mod, 'down' if down else 'up'))
    return down


def key(event):
    '''Other key pressed or released'''
    # print(event.keycode, event.keysym, event.down)
    global alt, ctrl, meta, shift
    t = [m for m in (alt, ctrl, shift, meta, str(event.keysym)) if m]
    print('+'.join(t))


def alt_key(down, *unused):
    '''Alt (aka Option on macOS) key is pressed or released'''
    global alt
    alt = up_down(_Alt, down)


def control_key(down, *unused):
    '''Control key is pressed or released'''
    global ctrl
    ctrl = up_down(_Ctrl, down)


def meta_key(down, *unused):
    '''Meta (aka Command on macOS) key is pressed or released'''
    global meta
    meta = up_down(_Meta, down)


def shift_key(down, *unused):
    '''Shift button is pressed or released'''
    global shift
    shift = up_down(_Shift, down)


def modifier(root, mod, handler, down):
    '''Add events and handlers for key press and release'''
    root.event_add('<<%sOn>>' % (mod,), ' <KeyPress-%s_L>' % (mod,), '<KeyPress-%s_R>' % (mod,))
    root.bind(     '<<%sOn>>' % (mod,), lambda _: handler('<%s>' % (down,)))

    root.event_add('<<%sOff>>' % (mod,), '<KeyRelease-%s_L>' % (mod,), '<KeyRelease-%s_R>' % (mod,))
    root.bind(     '<<%sOff>>' % (mod,), lambda _: handler(''))


root = tk.Tk()
root.geometry('256x64+0+0')

modifier(root, 'Alt',     alt_key,     _Alt)
modifier(root, 'Control', control_key, _Ctrl)
modifier(root, 'Meta',    meta_key,    _Meta)
modifier(root, 'Shift',   shift_key,   _Shift)

root.bind('<Key>', key)

root.mainloop()

With Tkinter, "Control-R" means Ctrl-Shift-R whereas "Control-r" means Ctrl-R. So make sure you're not mixing up uppercase and lowercase.


Option 1

Something like this:

# Status of control, shift and control+shift keys in Python
import tkinter as tk

ctrl = False
shift = False
ctrl_shift = False

def key(event):
    global ctrl, shift, ctrl_shift
    #print(event.keycode, event.keysym, event.state)
    if ctrl_shift:
        print('<Ctrl>+<Shift>+{}'.format(event.keysym))
    elif ctrl:
        print('<Ctrl>+{}'.format(event.keysym))
    elif shift:
        print('<Shift>+{}'.format(event.keysym))
    ctrl = False
    shift = False
    ctrl_shift = False

def control_key(state, event=None):
    ''' Controll button is pressed or released '''
    global ctrl
    ctrl = state

def shift_key(state, event=None):
    ''' Controll button is pressed or released '''
    global shift
    shift = state
    control_shift(state)

def control_shift(state):
    ''' <Ctrl>+<Shift> buttons are pressed or released '''
    global ctrl, ctrl_shift
    if ctrl == True and state == True:
        ctrl_shift = True
    else:
        ctrl_shift = False

root = tk.Tk()
root.geometry('256x256+0+0')

root.event_add('<<ControlOn>>',  '<KeyPress-Control_L>',   '<KeyPress-Control_R>')
root.event_add('<<ControlOff>>', '<KeyRelease-Control_L>', '<KeyRelease-Control_R>')
root.event_add('<<ShiftOn>>',    '<KeyPress-Shift_L>',     '<KeyPress-Shift_R>')
root.event_add('<<ShiftOff>>',   '<KeyRelease-Shift_L>',   '<KeyRelease-Shift_R>')

root.bind('<<ControlOn>>', lambda e: control_key(True))
root.bind('<<ControlOff>>', lambda e: control_key(False))
root.bind('<<ShiftOn>>', lambda e: shift_key(True))
root.bind('<<ShiftOff>>', lambda e: shift_key(False))
root.bind('<Key>', key)

root.mainloop()

Option 2

However, in the end, I decided to process keystrokes manually. You can se the example in this file. First, I set keycodes and shortcuts in two dictionaries self.keycode and self.__shortcuts:

# List of shortcuts in the following format: [name, keycode, function]
self.keycode = {}  # init key codes
if os.name == 'nt':  # Windows OS
    self.keycode = {
        'o': 79,
        'w': 87,
        'r': 82,
        'q': 81,
        'h': 72,
        's': 83,
        'a': 65,
    }
else:  # Linux OS
    self.keycode = {
        'o': 32,
        'w': 25,
        'r': 27,
        'q': 24,
        'h': 43,
        's': 39,
        'a': 38,
     }
self.__shortcuts = [['Ctrl+O', self.keycode['o'], self.__open_image],   # 0 open image
                    ['Ctrl+W', self.keycode['w'], self.__close_image],  # 1 close image
                    ['Ctrl+R', self.keycode['r'], self.__roll],         # 2 rolling window
                    ['Ctrl+Q', self.keycode['q'], self.__toggle_poly],  # 3 toggle between roi/hole drawing
                    ['Ctrl+H', self.keycode['h'], self.__open_poly],    # 4 open polygons for the image
                    ['Ctrl+S', self.keycode['s'], self.__save_poly],    # 5 save polygons of the image
                    ['Ctrl+A', self.keycode['a'], self.__show_rect]]    # 6 show rolling window rectangle

Then added self.__keystroke function to monitor <Ctrl> keystroke events. This function checks if <Ctrl> key is pressed or not:

def __keystroke(self, event):
    """ Language independent handle events from the keyboard """
    #print(event.keycode, event.keysym, event.state)  # uncomment it for debug purposes
    if event.state - self.__previous_state == 4:  # check if <Control> key is pressed
        for shortcut in self.__shortcuts:
            if event.keycode == shortcut[1]:
                shortcut[2]()
    else:  # remember previous state of the event
        self.__previous_state = event.state

Finally, bind the self.__keystroke function to the master GUI window. Note that this function is bonded in the idle mode, because multiple keystrokes slow down the program on weak computers:

# Handle keystrokes in the idle mode, because program slows down on a weak computers,
# when too many key stroke events in the same time.
self.master.bind('<Key>', lambda event: self.master.after_idle(self.__keystroke, event))