Multiple Key Event Bindings in Tkinter - "Control + E" "Command (apple) + E" etc - events
Mac OS X 10.6.6 - Tkinter
I want to bind multiple-key events, and while I have found an effbot article and the Tk man pages, I've been unable to make this work correctly. I'm new here.
I've had mixed success. I've been able to get Shift + letter key, but not Control or Command (Apple key). What I really want to do is Command + letter and Control + letter key so it would theoretically work in Windows and OS X.
I want it to work at window-level, so I'm using root. Perhaps there is a better way. Below is what I've tried:
root.bind('<Shift-E>', self.pressedCmdE) # Works
root.bind('e', self.pressedCmdE) # Works
root.bind('<Command-E>', self.pressedCmdE) # Does Not Work
#root.bind('<Mod1-E>', self.pressedCmdE) # # Do Mod1, M1, and
#root.bind('<M1-E>', self.pressedCmdE) # # Command mean the same thing?
Strangely, when I press alt/option + (E, N, or others) it creates an error. Is it interacting with PythonLauncher?
2011-06-16 16:19:22.618 Python[1546:d07] An uncaught exception was raised
2011-06-16 16:19:22.621 Python[1546:d07] *** -[NSCFString characterAtIndex:]: Range or index out of bounds
2011-06-16 16:19:22.622 Python[1546:d07] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSCFString characterAtIndex:]: Range or index out of bounds'
*** Call stack at first throw:
(
0 CoreFoundation 0x00007fff85b397b4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x00007fff848b90f3 objc_exception_throw + 45
2 CoreFoundation 0x00007fff85b395d7 +[NSException raise:format:arguments:] + 103
3 CoreFoundation 0x00007fff85b39564 +[NSException raise:format:] + 148
4 Foundation 0x00007fff866eb5e1 -[NSCFString characterAtIndex:] + 97
5 Tk 0x0000000100759bcf Tk_SetCaretPos + 663
6 Tk 0x000000010075fd94 Tk_MacOSXSetupTkNotifier + 699
7 Tcl 0x000000010061d2ae Tcl_DoOneEvent + 297
8 _tkinter.so 0x00000001001d9be9 init_tkinter + 1132
9 Python 0x0000000100089187 PyEval_EvalFrameEx + 15317
10 Python 0x000000010008acce PyEval_EvalCodeEx + 1803
11 Python 0x000000010008935e PyEval_EvalFrameEx + 15788
12 Python 0x000000010008acce PyEval_EvalCodeEx + 1803
13 Python 0x000000010008ad61 PyEval_EvalCode + 54
14 Python 0x00000001000a265a Py_CompileString + 78
15 Python 0x00000001000a2723 PyRun_FileExFlags + 150
16 Python 0x00000001000a423d PyRun_SimpleFileExFlags + 704
17 Python 0x00000001000b0286 Py_Main + 2718
18 Python 0x0000000100000e6c start + 52
)
terminate called after throwing an instance of 'NSException'
Abort trap
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.
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()
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))
Related
The wav file wont play after using pyinstaller --onefile.I just hear the windows 'beep'
This program displays a home circuit breaker panel. the user can view what is on each breaker on the panel (data taken from an imported dictionary of entered breaker panel info) or the user can check what breakers control any list zone (kitchen basement, etc) The breakerville program closes when the user decides and is supposed to play a wave file at the close. It doesn't play after the program is made into an exe with pyinstaller just the windows 'beep'. I am suspecting that I may need to edit the spec file to get the wave file to work after compiled. Is this correct and if so how? Do I need to modify the spec file? from playsound import playsound # CURRENTLY USING from chart import chart from BreakerZones import BreakerZones import time import sys import colorama import yaml # to print the nested_lookup results(n) on separate lines from nested_lookup import nested_lookup, get_all_keys # importing 2 items from nested_lookup from colorama import Fore, Back, Style colorama.init(autoreset=True) # If you don't want to print Style.RESET_ALL all the time, # reset automatically after each print statement with True print(colorama.ansi.clear_screen()) print('\n'*4) # prints a newline 4 times print(Fore.MAGENTA + ' Arriving-' + Fore.GREEN + ' *** BREAKERVILLE USA ***') def main(): print('\n' * 2) print(Fore.BLUE + ' Breaker Numbers and Zones') k = get_all_keys(BreakerZones) # raw amount of keys even repeats , has quotes new_l = [] # eliminate extra repeating nested keys for e in k: # has quotes if e not in new_l and sorted(e) not in new_l: # new_l.append(e) # print() new_l.sort() # make alphabetical newer_l = ('%s' % ', '.join(map(str, new_l)).strip("' ,")) # remove ['%s'] brackets so they don't show up when run print(' ', yaml.dump(newer_l, default_flow_style=False)) # strip("' ,") or will see leading "' ," in output print(Fore.BLUE + ' ENTER A BREAKER # OR ZONE', Fore.GREEN + ': ', end='') i = input().strip().lower() # these lines is workaround for the colorama print() # user input() issue of 'code' appearing in screen output if i in k: n = (nested_lookup(i, BreakerZones, wild=False, with_keys=False)) # wild=True means key not case sensitive, print(yaml.dump(n, default_flow_style=False)) # 'with_keys' returns values + keys also # for key, value in n.items(): eliminated by using yaml # print(key, '--', value) eliminated by using yaml else: print(Fore.YELLOW + ' Typo,' + Fore.GREEN + ' try again') main() print() print(Fore.GREEN + ' Continue? Y or N: C for breaker chart : ', end='') # see comments ENTER A BREAKER ans = input().strip().lower() # strip() removes any spaces before or after user input if ans == 'c': chart() print() print(Fore.GREEN + ' Continue? Y or N : ', end='') ans = input().strip().lower() # strip() removes any spaces before or after user input if ans == 'y': # shorter version 'continue Y or N' after printing breaker chart main() else: print() print(Fore.MAGENTA + ' Departing -' + Fore.GREEN + ' *** BREAKERVILLE ***') playsound('train whistle.wav') time.sleep(2) # delay to exit program sys.exit() elif ans != 'y': print() print(Fore.MAGENTA + ' Good Day -' + Fore.GREEN + ' *** BREAKERVILLE ***') playsound('train whistle.wav') #CURRENTLY USING time.sleep(2) # delay to exit program sys.exit() else: main() main()
For the records: The issue is fixed by providing full path to the sound file. This is probably linked to the implementation of playsound and how it determines what is the current working directory. Please refer to https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#run-time-information for a better understanding of that topic with pyinstaller
Logitech Script, 1st and 2nd click events with time reset
What I want to do is if I press the button on my mouse it uses a key like "E" and if I press the button again it uses the key "W" and after 2 seconds it resets, I mean if I don’t press the same button after 2 seconds it uses letter "e " again. Is that possible? I've tried some codes but no results yet: function OnEvent(event, arg, family) if event == "MOUSE_BUTTON_PRESSED" and arg == 5 then toggle = not toggle if toggle then PressKey("e") ReleaseKey("e") else PressKey("w") ReleaseKey("w") end end end
local prev_tm_btn5 = -math.huge function OnEvent(event, arg, family) if event == "MOUSE_BUTTON_PRESSED" and arg == 5 then local tm = GetRunningTime() local key = tm - prev_tm_btn5 > 2000 and "e" or "w" prev_tm_btn5 = tm PressKey(key) Sleep(15) ReleaseKey(key) end end
Tool/Algorithm for text comparision after every key hit
I am struggling to find a text comparison tool or algorithm that can compare an expected text against the current state of the text being typed. I will have an experimentee typewrite a text that he has in front of his eyes. My idea is to compare the current state of the text against the expected text whenever something is typed. That way I want to find out when and what the subject does wrong (I also want to find errors that are not in the resulting text but were in the intermediate text for some time). Can someone point me in a direction? Update #1 I have access to the typing data in a csv format: This is example output data of me typing "foOBar". Every line has the form (timestamp, Key, Press/Release) 17293398.576653,F,P 17293398.6885,F,R 17293399.135282,LeftShift,P 17293399.626881,LeftShift,R 17293401.313254,O,P 17293401.391732,O,R 17293401.827314,LeftShift,P 17293402.073046,O,P 17293402.184859,O,R 17293403.178612,B,P 17293403.301748,B,R 17293403.458137,LeftShift,R 17293404.966193,A,P 17293405.077869,A,R 17293405.725405,R,P 17293405.815159,R,R
In Python Given your input csv file (I called it keyboard_records.csv) 17293398.576653,F,P 17293398.6885,F,R 17293399.135282,LeftShift,P 17293399.626881,LeftShift,R 17293401.313254,O,P 17293401.391732,O,R 17293401.827314,LeftShift,P 17293402.073046,O,P 17293402.184859,O,R 17293403.178612,B,P 17293403.301748,B,R 17293403.458137,LeftShift,R 17293404.966193,A,P 17293405.077869,A,R 17293405.725405,R,P 17293405.815159,R,R The following code does the following: Read its content and store it in a list named steps For each step in steps recognizes what happened and If it was a shift press or release sets a flag (shift_on) accordingly If it was an arrow pressed moves the cursor (index of current where we insert characters) – if it the cursor is at the start or at the end of the string it shouldn't move, that's why those min() and max() If it was a letter/number/symbol it adds it in curret at cursor position and increments cursor Here you have it import csv steps = [] # list of all actions performed by user expected = "Hello" with open("keyboard.csv") as csvfile: for row in csv.reader(csvfile, delimiter=','): steps.append((float(row[0]), row[1], row[2])) # Now we parse the information current = [] # text written by the user shift_on = False # is shift pressed cursor = 0 # where is the cursor in the current text for step in steps: time, key, action = step if key == 'LeftShift': if action == 'P': shift_on = True else: shift_on = False continue if key == 'LeftArrow' and action == 'P': cursor = max(0, cursor-1) continue if key == 'RightArrow' and action == 'P': cursor = min(len(current), cursor+1) continue if action == 'P': if shift_on is True: current.insert(cursor, key.upper()) else: current.insert(cursor, key.lower()) cursor += 1 # Now you can join current into a string # and compare current with expected print(''.join(current)) # printing current (just to see what's happening) else: # What to do when a key is released? # Depends on your needs... continue To compare current and expected have a look here. Note: by playing around with the code above and a few more flags you can make it recognize also symbols. This will depend on your keyboard. In mine Shift + 6 = &, AltGr + E = € and Ctrl + Shift + AltGr + è = {. I think this is a good point to start. Update Comparing 2 texts isn't a difficult task and you can find tons of pages on the web about it. Anyway I wanted to present you an object oriented approach to the problem, so I added the compare part that I previously omitted in the first solution. This is still a rough code, without primary controls over the input. But, as you asked, this is pointing you in a direction. class UserText: # Initialize UserText: # - empty text # - cursor at beginning # - shift off def __init__(self, expected): self.expected = expected self.letters = [] self.cursor = 0 self.shift = False # compares a and b and returns a # list containing the indices of # mismatches between a and b def compare(a, b): err = [] for i in range(min(len(a), len(b))): if a[i] != b[i]: err.append(i) return err # Parse a command given in the # form (time, key, action) def parse(self, command): time, key, action = command output = "" if action == 'P': if key == 'LeftShift': self.shift = True elif key == 'LeftArrow': self.cursor = max(0, self.cursor - 1) elif key == 'RightArrow': self.cursor = min(len(self.letters), self.cursor + 1) else: # Else, a letter/number was pressed. Let's # add it to self.letters in cursor position if self.shift is True: self.letters.insert(self.cursor, key.upper()) else: self.letters.insert(self.cursor, key.lower()) self.cursor += 1 ########## COMPARE WITH EXPECTED ########## output += "Expected: \t" + self.expected + "\n" output += "Current: \t" + str(self) + "\n" errors = UserText.compare(str(self), self.expected[:len(str(self))]) output += "\t\t" i = 0 for e in errors: while i != e: output += " " i += 1 output += "^" i += 1 output += "\n[{} errors at time {}]".format(len(errors), time) return output else: if key == 'LeftShift': self.shift = False return output def __str__(self): return "".join(self.letters) import csv steps = [] # list of all actions performed by user expected = "foobar" with open("keyboard.csv") as csvfile: for row in csv.reader(csvfile, delimiter=','): steps.append((float(row[0]), row[1], row[2])) # Now we parse the information ut = UserText(expected) for step in steps: print(ut.parse(step)) The output for the csv file above was: Expected: foobar Current: f [0 errors at time 17293398.576653] Expected: foobar Current: fo [0 errors at time 17293401.313254] Expected: foobar Current: foO ^ [1 errors at time 17293402.073046] Expected: foobar Current: foOB ^^ [2 errors at time 17293403.178612] Expected: foobar Current: foOBa ^^ [2 errors at time 17293404.966193] Expected: foobar Current: foOBar ^^ [2 errors at time 17293405.725405]
I found the solution to my own question around a year ago. Now i have time to share it with you: In their 2003 paper 'Metrics for text entry research: An evaluation of MSD and KSPC, and a new unified error metric', R. William Soukoreff and I. Scott MacKenzie propose three major new metrics: 'total error rate', 'corrected error rate' and 'not corrected error rate'. These metrics have become well established since the publication of this paper. These are exaclty the metrics i was looking for. If you are trying to do something similiar to what i did, e.g. compare the writing performance on different input devices this is the way to go.
How can I read the arrow keys in a Ruby curses application?
I have a Ruby curses application in which I'd like to trap for the arrow keys and function keys. The problem is that some keystrokes generate multiple values when using STDIN.getch. When I type a 'regular' key like a-z I get a single value back. When I type a [F]key or arrow key I get three values back. Is there a gem designed for handling keyboard input or a better way to accomplish reading keystrokes? #!/usr/bin/ruby require 'curses' require 'io/console' Curses.noecho Curses.init_screen main_window = Curses::Window.new(24, 40, 1, 0) num_keys = 0 loop do ch = STDIN.getch num_keys = num_keys + 1 main_window.addstr(' key:' + ch.inspect + ' count:' + num_keys.to_s) main_window.refresh break if ch == 'q' end Curses.close_screen
Trying enabling the keypad on the window right after you instantiate it. main_window = Curses::Window.new(24, 40, 1, 0) main_window.keypad = true and then instead of using STDIN.getch there's a getch method on the window as well you can use, so try changing ch = STDIN.getch to ch = main_window.getch now when I run your program, I get key: 259 count: 1 when I hit the up arrow instead of key:"\e" count 1 key:"[" count:2 key:"A" count:3
KEY_DOWN not working in CURSES
I'm building a curses module and using KEY_DOWN to check if a arrow down key is pressed. But, I get a Name error saying KEY_DOWN is not defined. if value == KEY_DOWN: NameError: global name 'KEY_DOWN' is not defined
Good day! You have to do: if value == curses.KEY_DOWN: for it to work. Hope this works!!! But if this doesn't work show us your code (so we can analyze it)
To follow up to mvndaai's answer, if you want to detect arrow keys in Python, you have to AND together the three different ASCII values. For example: key = getch() if key == (27 and 91 and 65): #27 is ESC, 91 is [, and 65 is A print("Up key pressed!") if key == (27 and 91 and 66): print("Down key pressed!") if key == (27 and 91 and 67): print("Right key pressed!") if key == (27 and 91 and 68): print("Left key pressed!")
I am not sure why the yave a gloabl named KEY_DOWN, but if you want a key down, you need to do 3 getchs. Warning, the first getch is the same as an ESC. That means you either make sure it doesn't close on ESC or do a work around like I did below. I also included a chart of what you will get as a getch for each key. KeyESCUPDOWNRIGHTLEFT getch2727272727 getch [[[[ getch ABCD Which means that when you hit any Arrow key and you are triggering a ESC. In ruby I tried a work around of checking for 27, then doing a quick timeout on another getch. If that gets a [ it is an arrow or something else, otherwise it is the escape key. Here is my ruby code: def read_key ch = getch return check_escape_chars if ch == 27 ch end def check_escape_chars require 'timeout' begin Timeout.timeout(0.0001) {getch} case getch when "A"; return "UP" when "B"; return "DOWN" when "C"; return "RIGHT" when "D"; return "LEFT" end rescue Timeout::Error return "ESC" end end