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))