oskros
oskros

Reputation: 3305

Monitor keypress in tkinter without focus

I am writing a small timing module in Python using tkinter. In that regard, I would like to globally monitor when the escape key is pressed for stopping the timing.

Unfortunately, tkinters ".bind" and ".bind_all" functions only pick up keystrokes when the window is in focus.

I have looked at several other solutions for logging keystrokes including the packages "keyboard" and "pynput", however these packages requires running a while loop which makes the tkinter GUI freeze up and stop working.

I found this thread, but it is not very helpful for specifically showing how it can be done: Detect keypress without drawing canvas or frame on tkinter

Some different options i tried

Option 1: Using the tkinter loop function, but doesn't register when key is pressed

import keyboard
def _check_esc_pressed(self):
    if self.run_active and keyboard.press('esc'):
        self.Lap()
        self.Stop()
    self.after(50, self._check_esc_pressed())

Option 2: Freezes the tkinter client

import keyboard  
def _check_esc_pressed(self):
    while True:  
        if keyboard.is_pressed('esc'): 
            self.Lap()
            self.Stop()
            break  
        else:
            pass

Option 3: Freezes the tkinter client

from pynput.keyboard import Key, Listener
def on_release(self, key):
    if key == Key.esc:
        self.Lap()
        self.Stop()
        # Stop listener
        return False

def _check_esc_pressed(self):
    def on_press(key):
        pass
    with Listener(
            on_press=on_press,
            on_release=on_release) as listener:
        listener.join()

I expect that pressing escape will terminate the "_check_esc_pressed" function, register a lap and stop the timer. The check for escape should only be processed while a run is active

Upvotes: 0

Views: 2791

Answers (3)

Ariel Montes
Ariel Montes

Reputation: 276

I encountered some issues using the keyboard module on Linux because it requires sudo privileges to run. This is not the case for the pynput module. Therefore, I am providing this alternative example:

from pynput import keyboard
import tkinter as tk

if __name__ == "__main__":
    # Create the GUI
    root = tk.Tk()
    root.geometry("170x60")
    lbl_1 = tk.Label(root, text="Hello World!")
    lbl_1.pack()
    btn_1 = tk.Button(root, text="Exit")
    btn_1.pack()    

    # Create tk event handler
    def lbl_1_handler(event):
        lbl_1.config(text = "GoodBye World!")

    # Bind event handler to button click
    btn_1.bind("<Button-1>",lbl_1_handler)

    # Bind event handler to key pressed
    def on_press(key):
        # Check if esc key has been pressed
        if key == keyboard.Key.esc:
            lbl_1_handler(None)

    # The `keyboard.Listener` creates a thread that listens for
    # key presses even when the tk window is not focused.
    listener = keyboard.Listener(
        on_press=on_press)
    listener.start()

    # Run the GUI main loop
    root.mainloop()

Upvotes: 1

oskros
oskros

Reputation: 3305

I found a solution to the issue by using the system_hotkey package for python. This package allows you to assign system-wide hotkeys that work without focus on the tkinter program.

from system_hotkey import SystemHotkey
hk = SystemHotkey()
hk.register(['alt', 'q'], callback=lambda event: self.Start())
hk.register(['alt', 'w'], callback=lambda event: self.Stop())

Bear in mind that any hotkey added like this will make the registered hotkey combination inaccessible to other programs.

Upvotes: 1

Rhubarb
Rhubarb

Reputation: 41

In case you still need an answer, or others find this useful...

The answer may lie in the code we can't see - By putting the app's logic in another thread, it allows tkinter to do its thing without freezing.

The code below follows your (slightly modified) option 2:

import queue
import keyboard  
import threading
import time
import tkinter as tk

def app_main_loop(my_label):
    # Create another thread that monitors the keyboard
    input_queue = queue.Queue()
    kb_input_thread = threading.Thread(target=_check_esc_pressed, args=(input_queue,))
    kb_input_thread.daemon = True
    kb_input_thread.start()
    
    # Main logic loop
    run_active = True
    while True:
        if not input_queue.empty():
            if (run_active) and (input_queue.get() == "esc"):
                run_active = False
                Lap(my_label)
                Stop()
        time.sleep(0.1)  # seconds

def _check_esc_pressed(input_queue):
    while True:
        if keyboard.is_pressed('esc'):
            input_queue.put("esc")
        time.sleep(0.1) # seconds

def Lap(my_label):
    my_label.configure(text = "Lap")

def Stop():
    print("Stopped")

if __name__ == "__main__":
    # Create the ui
    root = tk.Tk()
    root.attributes("-fullscreen", True)
    my_label = tk.Label(root, text="Hello World!")
    my_label.pack()
    
    # Run the app's main logic loop in a different thread
    main_loop_thread = threading.Thread(target=app_main_loop, args=(my_label, ))
    main_loop_thread.daemon = True
    main_loop_thread.start()
    
    # Run the UI's main loop
    root.mainloop()

Upvotes: 3

Related Questions