4

I have a python program which uses tkinter and key bindings. The program calls an external program and then asks, what to do with it, expecting n-key for "no" or y-key for "yes". The problem: If I press the allowed key twice, the second key is stored and processed later on - thus for the wrong question.

import tkinter as tk
import time

class App:
    counter = 0

    def __init__(self, master): # Constructor
        # build GUI
        self.label = tk.Label(text="Press <space>", width=40)
        self.label.grid(row=1, column=1, sticky='w')

        # bind keys to buttons
        master.bind('<Key-space>', self.keyPressed)
        pass # __init__


    def keyPressed(self, event):
        print("Step 1")
        self.counter = self.counter + 1
        self.label.configure(text = str(self.counter))
        self.label.update()
        time.sleep(3)
        print("Step  2")
        pass # Key1Pressed
# End of class App

root = tk.Tk()
root.option_add('*font', ('Arial', 11, 'bold'))
# root.attributes("-toolwindow", 1)

posX = root.winfo_screenwidth() - 500
posY = 30
root.geometry("+%d+%d" % (posX, posY))

root.title("Bind tester")
display = App(root)
root.mainloop()

In the code snippet above I replaced the external program by sleep() and the key binding for the spacebar. If the python3 skript is started and space is pressed two times within the sleep period, I see outputs in the terminal like:

Step 1
Step  2
Step 1
Step  2

I would like to ignore any key event, that was raised during the sleep routine. So next I tried to use a semaphore, but this was also not successful. The problem might be more complex, because if I press space three times within the sleep period, I'll get the following result:

Step 1
Step  2
Step 1
Step 1
Step  2
Step  2

Looks as if the function keypressed is called multiple times in parallel. Is there a possibility to clear the event queue before accepting new events?

DirkS
  • 41
  • 1
  • 2

2 Answers2

5

This requires one of Tkinter's little known quirks, i.e. you have to return "break" from a function. The program below rebinds the key to the "ignore()" function that returns "break". I don't understand how or why this is, and it is documented http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm and does work.

import sys
if sys.version_info[0] < 3:
    import Tkinter as tk     ## Python 2.x
else:
    import tkinter as tk     ## Python 3.x

class App:
    def __init__(self, master): # Constructor
        self.master=master
        self.counter=0
        # build GUI
        self.label = tk.Label(text="Press <space>", width=40)
        self.label.grid(row=1, column=1, sticky='w')

        # bind keys to buttons
        master.bind('<Key-space>', self.keyPressed)


    def ignore(self, event):
        print "ignore"
        return "break"

    def keyPressed(self, event):
        self.master.bind('<Key-space>', self.ignore)
        print("Step 1")
        self.counter = self.counter + 1
        self.label["text"] = str(self.counter)
        self.master.after(3000, self.bindit)
        print("Step  2")

    def bindit(self):
        self.master.bind('<Key-space>', self.keyPressed)
        print("Step 3 = ready for more input")

root = tk.Tk()
root.option_add('*font', ('Arial', 11, 'bold'))
# root.attributes("-toolwindow", 1)

posX = root.winfo_screenwidth() - 500
posY = 30
root.geometry("+%d+%d" % (posX, posY))

root.title("Bind tester")
display = App(root)
root.mainloop()
  • 1
    The "how" and "why" is pretty simple. See this answer for an explanation: http://stackoverflow.com/questions/11541262/basic-query-regarding-bindtags-in-tkinter/11542200#11542200 – Bryan Oakley May 17 '15 at 23:23
  • Thank's, your solution work. Now I have to implement your recommendations in my code. I have to use 'after' to finish my method and redirect other events to a method with 'break'. Perhaps I can also try to implement it in the original keyPressed method using a semaphore. – DirkS May 18 '15 at 13:15
  • You just rebind the key to ignore() before running the external program. After subprocess (or whatever you use) returns you can rebind the key to keyPressed(). You can eliminate the after() as that was just to simulate the time for the external program to run. –  May 18 '15 at 20:58
  • 1
    Thank you for your solution. In my case, doing `return "break"` is suffice to prevent `tkinter.Entry` widget from auto-selecting text after inserting new text for an autocomplete feature. – tom_mai78101 Jun 21 '16 at 22:11
1

Probably the time.sleep method is messing with Tkinter mainloop. You should use the 'after' method from Tkinter module.

http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html

If you do not pass a callback argument, this method waits delay_ms milliseconds, as in the .sleep() function of the standard Python time module.

Check this post for an example:

Python time.sleep

Community
  • 1
  • 1
  • The after-method does the same, but to clarify: I don't use sleep in reality. Instead I call an external program via subprocess.call and wait until it has finished its job. I need a function to control the event queue – DirkS May 16 '15 at 11:47
  • There's no mystery here -- time.sleep doesn't "mess" with the event loop, it simply prevents it from running. – Bryan Oakley May 17 '15 at 23:35