Reputation: 2391
I understood that the Tk keypress and keyrelease events were supposed only to fire when the key was actually pressed or released?
However with the following simple code, if I hold down the "a" key I get a continual sequence of alternating keypress/keyrelease events.
Am I doing something wrong or is TkInter buggy? This is Python2.7 on Linux mint.
from Tkinter import *
def keyup(e):
print 'up', e.char
def keydown(e):
print 'down', e.char
root = Tk()
frame = Frame(root, width=100, height=100)
frame.bind("<KeyPress>", keydown)
frame.bind("<KeyRelease>", keyup)
frame.pack()
frame.focus_set()
root.mainloop()
Output when pressing and holding "a":
down a
up a
down a
up a
down a
up a
down a
up a
etc...
Upvotes: 43
Views: 96416
Reputation: 1
Another version in Python 3 for multiple keys. The key up is not so accurate and can be improved.
from tkinter import *
import threading
import time
def on_key_press(e):
print('up', e.char)
def on_key_release(e):
print('down', e.keysym)
class Key():
def __init__(self, key):
self.last_press_time = 0
self.last_release_time = 0
self.key = key
self.released = False
class KeyTracker():
DELTA = 0.015
def __init__(self, keys):
self.keys = keys
self.key = self.keys[0]
def find(self, key):
for k in self.keys:
if k.key == key:
return k
return
def press_checker(self, event):
self.key = self.find(event.keysym)
if self.key == None:
return
self.key.last_press_time = time.time()
if self.key.last_press_time - self.key.last_release_time > self.DELTA:
self.key.released = False
on_key_press(event)
def release_checker(self, event):
self.key = self.find(event.keysym)
if self.key == None:
return
self.key.last_release_time = time.time()
threading.Timer(self.DELTA,
self.release_debounce,
args=[event]).start()
def release_debounce(self, event):
if self.key.last_release_time > self.key.last_press_time and not self.key.released:
self.key.released = True
on_key_release(event)
window = Tk()
key_tracker = KeyTracker([Key('a'),Key('s'),Key('d')])
window.bind('<KeyPress>', key_tracker.press_checker)
window.bind('<KeyRelease>', key_tracker.release_checker)
window.mainloop()
Upvotes: 0
Reputation: 11
like Jeffrey answered you could implement debouncing, but, his answer has an inaccuracy, so I'd try to improve and to clarify his answer,
So, the problem is key autorepeating, we can check timings with such sample code:
import tkinter
import time
last = 0
def on_key_press(event):
global last
print('down', time.time() - last)
last = time.time()
def on_key_release(event):
global last
print('up', time.time() - last)
last = time.time()
window = tkinter.Tk()
window.bind("<KeyPress>", on_key_press)
window.bind("<KeyRelease>", on_key_release)
window.mainloop()
output should be something like that:
down 1709468301.8255844
up 0.24304962158203125
down 0.000774383544921875
up 0.018041133880615234
down 0.0007359981536865234
up 0.017796754837036133
down 0.0006606578826904297
up 0.018764734268188477
So, we have this key repeating pattern:
_______ _ _ _ _ _______
\_________/ \____/ \____/ \___/ \____/
^ ^ ^
first press bouncing last release of key
And now all we need to do is check that between press and previous release passed time to confirm that this is separate press and not a bounce:
When key is pressed - we check that the button has not been released too recently before the press
When key is released - it is a little trickier, because we need to check that the button will not be pressed in the nearest time. We don't know the future at the time of releasing the key, so we need to create timer that will call checker function which will verify, that there was not ant repeated pressed in small time interval, is if so - we confirm the release event.
from tkinter import *
import threading
import time
def on_key_press(e):
print('up', e.char)
def on_key_release(e):
print('down', e.char)
class KeyTracker():
DELTA = 0.05
def __init__(self, key):
self.last_press_time = 0
self.last_release_time = 0
self.key = key
self.released = False
def press_checker(self, event):
if event.keysym != self.key:
return
self.last_press_time = time.time()
if self.last_press_time - self.last_release_time > self.DELTA:
self.released = False
on_key_press(event)
def release_checker(self, event):
if event.keysym != self.key:
return
self.last_release_time = time.time()
threading.Timer(self.DELTA,
self.release_debounce,
args=[event]).start()
def release_debounce(self, event):
if self.last_release_time > self.last_press_time and not self.released:
self.released = True
on_key_release(event)
window = Tk()
key_tracker = KeyTracker('space')
window.bind('<KeyPress>', key_tracker.press_checker)
window.bind('<KeyRelease>', key_tracker.release_checker)
window.mainloop()
Also, if the repeat rate is big, we can get a situation, when we have several timers pending, and after the final release of the key - we will have multiple release events, so we need to check this with additional released
variable, so repeated events will be filtered
Upvotes: 1
Reputation: 195
If you want to track only one key, you can do that:
import tkinter as tk
class KeyTracker:
def __init__(self, on_key_press, on_key_release):
self.on_key_press = on_key_press
self.on_key_release = on_key_release
self._key_pressed = False
def report_key_press(self, event):
if not self._key_pressed:
self.on_key_press()
self._key_pressed = True
def report_key_release(self, event):
if self._key_pressed:
self.on_key_release()
self._key_pressed = False
def start_recording(event=None):
print('Recording right now!')
def stop_recording(event=None):
print('Stop recording right now!')
if __name__ == '__main__':
master = tk.Tk()
key_tracker = KeyTracker(start_recording, stop_recording)
master.bind("<KeyPress-Return>", key_tracker.report_key_press)
master.bind("<KeyRelease-Return>", key_tracker.report_key_release)
master.mainloop()
Upvotes: 0
Reputation: 2406
The trick is to track the fact that a key there is a key down, what keys are currently down, and the fact that there is no longer a key pressed. All while ignoring the keyboard repeater.
This little prototype should cover all of the bases:
#Key press prototype
#Tracks keys as pressed, ignoring the keyboard repeater
#Current keys down are kept in a dictionary.
#That a key is pressed is flagged, and the last key pressed is tracked
import tkinter
winWid = 640
winHei = 480
keyDown = False
lastKey = "none"
keyChange = keyDown
keyList = {}
def onKeyDown(event):
global keyDown, lastKey, keyList
if (event.char in keyList) != True:
keyList[event.char] = "down"
print(keyList)
keyDown = True
lastKey = event.char
def onKeyUp(event):
global keyDown
if (event.char in keyList) == True:
keyList.pop(event.char)
if len(keyList) == 0:
keyDown = False
print(keyList)
#onTimer is present to show keyboard action as it happens.
#It is not needed to track the key changes, and it can be
#removed.
def onTimer():
global keyChange, timerhandle
if keyDown != keyChange:
keyChange = keyDown
if keyDown:
print("Key down, last key pressed - " + lastKey)
else:
print("Key up, last key pressed - " + lastKey)
timerhandle = window.after(20,onTimer)
def onShutdown():
window.after_cancel(timerhandle)
window.destroy()
window = tkinter.Tk()
frame = tkinter.Canvas(window, width=winWid, height=winHei, bg="black")
frame.pack()
frame.bind("<KeyPress>", onKeyDown)
frame.bind("<KeyRelease>", onKeyUp)
frame.focus_set()
timerhandle = window.after(20,onTimer)
window.protocol("WM_DELETE_WINDOW",onShutdown)
window.mainloop()
Upvotes: 3
Reputation: 173
Well, this is a bit late now, but I have a solution that works. It's not great, but it does not require os.system overwriting system settings, which is nice.
Basically, I make a class that records the timing of key presses. I say that a key is down when it has been pressed in the last short amount of time (here, .1ms). To get a press, it is easy enough: if the key is not registered as pressed, trigger the event. For releases, the logic is harder: if there is a suspected release event, set a timer for a short time (here, .1s) and then check to make sure the key is not down.
Once you have validated a press or release, call the on_key_press or on_key_release methods in your code. As for those, just implement them the way you originally wanted them
I know this is not perfect, but I hope it helps!!
Here is the code:
Where you are initializing keypress events:
key_tracker = KeyTracker()
window.bind_all('<KeyPress>', key_tracker.report_key_press)
window.bind_all('<KeyRelease>', key_tracker.report_key_release)
key_tracker.track('space')
Here is my custom KeyTracker class:
class KeyTracker():
key = ''
last_press_time = 0
last_release_time = 0
def track(self, key):
self.key = key
def is_pressed(self):
return time.time() - self.last_press_time < .1
def report_key_press(self, event):
if event.keysym == self.key:
if not self.is_pressed():
on_key_press(event)
self.last_press_time = time.time()
def report_key_release(self, event):
if event.keysym == self.key:
timer = threading.Timer(.1, self.report_key_release_callback, args=[event])
timer.start()
def report_key_release_callback(self, event):
if not self.is_pressed():
on_key_release(event)
self.last_release_time = time.time()
Upvotes: 7
Reputation:
how about;
from Tkinter import *
wn = Tk()
wn.title('KeyDetect')
m = 0
def down(e):
if m == 0:
print 'Down\n', e.char, '\n', e
global m
m = 1
def up(e):
if m == 1:
print 'Up\n', e.char, '\n', e
global m
m = 0
wn.bind('<KeyPress>', down)
wn.bind('<KeyRelease>', up)
wn.mainloop()
now it won't repeat.
Upvotes: 1
Reputation: 19209
Autorepeat behavior is system dependent. In Win7,
down a
down a
down a
...
down a
up a
This is for less than a second.
Upvotes: 5
Reputation: 2391
Ok some more research found this helpful post which shows this is occuring because of X's autorepeat behaviour. You can disable this by using
os.system('xset r off')
and then reset it using "on" at the end of your script. The problem is this is global behaviour - not just my script - which isn't great so I'm hoping someone can come up with a better way.
Upvotes: 23