Search code examples
pythonwindowskeyboardkeyboard-shortcuts

Listen for a shortcut (like WIN+A) even if the Python script does not have the focus


I'd like a Python script to run constantly in background and do something when a certain keyboard shortcut is pressed, such as WIN+A.

I've read Key Listeners in python? and solutions using pynput, but it seems to work and detect key presses when the window/console has focus.

Question: how to detect a keyboard shortcut in Python, such as WIN+A, and launch a function when it happens, even if the focus is somewhere else (e.g. browser, etc.)?

Note: my OS is Windows. Also I would prefer that the Python script only "registers" listening to WIN+A (does this exist?), and does not listen to all keypresses (otherwise it would be more or less a keylogger, which I don't want!).


Here is what I have tried:

import pyHook, pythoncom

def OnKeyboardEvent(event):
    if event.Ascii == 8:  # backspace 
        print('hello')
    return True
    
hm = pyHook.HookManager()
hm.KeyDown = OnKeyboardEvent
hm.HookKeyboard()
pythoncom.PumpMessages()

I would like to avoid it for 2 reasons: first, I find it very intrusive to listen to all keypresses and secondly, this common example about pyhook I found in many places has a bug: TypeError: KeyboardSwitch() missing 8 required positional arguments: 'msg', 'vk_ code', 'scan_code', 'ascii', 'flags', 'time', 'hwnd', and 'win_name'. The new version PyHook3 doesn't seem to work for Py36-64 either: pip install PyHook3 fails on Windows.


Solution

  • In fact, I was wrong: pynput is indeed able to detect key presses globally, and not only for the currently active window.

    1) Using pynput

    This following code launches the application notepad with WIN+W and calc with WIN+C:

    from pynput import keyboard
    import subprocess
    
    pressed = set()
    
    COMBINATIONS = [
        {
            "keys": [
                {keyboard.Key.cmd, keyboard.KeyCode(char="w")},
                {keyboard.Key.cmd, keyboard.KeyCode(char="W")},
            ],
            "command": "notepad",
        },
        {
            "keys": [
                {keyboard.Key.cmd, keyboard.KeyCode(char="c")},
                {keyboard.Key.cmd, keyboard.KeyCode(char="C")},
            ],
            "command": "calc",
        },
    ]
    
    def run(s):
        subprocess.Popen(s)
    
    def on_press(key):
        pressed.add(key)
        print(pressed)
        for c in COMBINATIONS:
            for keys in c["keys"]:
                if keys.issubset(pressed):
                    run(c["command"])
    
    def on_release(key):
        if key in pressed:
            pressed.remove(key)
    
    with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
        listener.join()
    

    2) Using ctypes and win32con

    This is inspired by this post cited by @fpbhb in a comment. It listens to WIN+F3 and WIN+F4.

    import os, sys
    import ctypes
    from ctypes import wintypes
    import win32con
    
    def handle_win_f3():
        os.startfile(os.environ["TEMP"])
    
    def handle_win_f4():
        ctypes.windll.user32.PostQuitMessage(0)
    
    HOTKEYS = [
        {"keys": (win32con.VK_F3, win32con.MOD_WIN), "command": handle_win_f3},
        {"keys": (win32con.VK_F4, win32con.MOD_WIN), "command": handle_win_f4},
    ]
    
    for i, h in enumerate(HOTKEYS):
        vk, modifiers = h["keys"]
        print("Registering id", i, "for key", vk)
        if not ctypes.windll.user32.RegisterHotKey(None, i, modifiers, vk):
            print("Unable to register id", i)
    try:
        msg = wintypes.MSG()
        while ctypes.windll.user32.GetMessageA(ctypes.byref(msg), None, 0, 0) != 0:
            if msg.message == win32con.WM_HOTKEY:
                HOTKEYS[msg.wParam]["command"]()
            ctypes.windll.user32.TranslateMessage(ctypes.byref(msg))
            ctypes.windll.user32.DispatchMessageA(ctypes.byref(msg))
    finally:
        for i, h in enumerate(HOTKEYS):
            ctypes.windll.user32.UnregisterHotKey(None, i)
    

    3) Using keyboard

    See https://github.com/boppreh/keyboard. It is interesting, but for me it had some drawbacks compared to other solutions: some fast keystrokes (while CPU-intensive processing is done) were not caught. They were caught when using solution 1) or 2).


    It seems there is no 100% Tkinter solution, see Capture all keypresses of the system with Tkinter.