Search code examples
pythontkinterkeypresstkinter-entry

Calling a function after time period has elapsed since last key press in tkinter


I have an entry in tkinter application in Python. If user changes the entry content, program reacts via onValidate function.

Now I would like to select all text in the entry field, if user has not pressed any keys for a while.

Below I have tried to select_all_text after 1000 ms using after function, but it seems not to work.

import tkinter as tk  

class MyEntry(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        self.vcmd = (self.register(self.onValidate),
                        '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')

        self.entry = tk.Entry(self, validate="key", validatecommand=self.vcmd)
        self.entry.pack(side="top", fill="x")

        self.select_all_text()

    def select_all_text(self):
        self.entry.focus()
        self.entry.select_range(0,'end')

    def onValidate(self, d, i, P, s, S, v, V, W):
        self.root.after(1000, self.select_all_text)

        return True

if __name__ == "__main__":
    root = tk.Tk()
    MyEntry(root).pack(fill="both", expand=True)

    root.mainloop()

Also the timer should reset each time a new key is pressed, so that select_all_text is called only after a long enough time has elapsed since last key press. In the example below select_all_text is called after each key press, which is not desired behavior.

I was thinking that could one track the time since last key press, and if it exceeds a certain threshold, then function would be called. That kind of method would also solve the problem.

How to call a function if a long enough time has elapsed since last key press in tkinter?


Solution

  • When you keep a reference to the id that after returns, you can cancel it with after_cancel. With this, you can cancel the scheduled function and reschedule it on every keypress:

    class MyEntry(tk.Frame):
    
        def __init__(self, parent):
            tk.Frame.__init__(self, parent)
            self.parent = parent
            self.after_id = None
    
            ...
    
        def onValidate(self, d, i, P, s, S, v, V, W):
            if self.after_id:
                self.parent.after_cancel(self.after_id)
            self.after_id = self.parent.after(1000, self.select_all_text)
    
            return True
    

    Note that I also saved the root window that you pass as the argument to MyEntry in self.parent and used that to call after on.


    P.S. You don't really need the Entry's validatecommand command to do this (but you could if you already use the validatecommand anyway of course). You can bind the function to every keypress:

    class MyEntry(tk.Frame):
    
        def __init__(self, parent):
            tk.Frame.__init__(self, parent)
            self.parent = parent
            self.after_id = None
    
            self.entry = tk.Entry(self)
            self.entry.bind('<Key>', self.entry_keypress)
    
            ...
    
        def entry_keypress(self, e):
            print(self.after_id)
            if self.after_id:
                self.parent.after_cancel(self.after_id)
            self.after_id = self.parent.after(1000, self.select_all_text)