32
votes

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...
5

5 Answers

21
votes

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.

5
votes

Autorepeat behavior is system dependent. In Win7,

down a
down a
down a
...
down a
up a

This is for less than a second.

4
votes

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()
2
votes

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.

0
votes

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