lost
lost

Reputation: 2391

Tkinter KeyPress and KeyRelease events

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

Answers (8)

lrpuppi
lrpuppi

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

tlucanti
tlucanti

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

th0mash
th0mash

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

codingCat
codingCat

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

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

user9162065
user9162065

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

Terry Jan Reedy
Terry Jan Reedy

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

lost
lost

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

Related Questions