Is there any way to make tkinter window dismiss Win + D command?

33 views Asked by At

I'm trying to make some application with python tkinter, that must always stay on the top of the screen. I searched a lot of documents, and I finally know how to set tkinter window to stay on the top, but couldn't find the way to stay top even though the user presses Win + D shortcut.

Is there any way to make tkinter window dismiss Win + D command?

1

There are 1 answers

0
JRiggles On

You can set up a low-level keyboard hook to essentially "absorb" any forbidden keypresses, though it may be a bit overkill. In this example, I've set up a filter for the left and right Windows keys.

This relies on the pywin32 module, but could likely be rewritten to use ctypes exclusively.

For what it's worth, there are caveats:

  • this is a fairly heavy-handed approach, and will prevent any/all Windows-key-related shortcuts from working for as long as it's running
  • it will perform this filtering OS-wide, not just in your application

This code is largely adapted from this answer, and the original author also provides a keyboard module that abstracts away some of this technical kung-fu - it's probably worth looking into.

With all that out of the way, the idea here is that you set up an instance of the KeyFilter class and target the KeyFilter.set_hook method to run in its own thread, and the keys you set up in the ForbiddenKeys enum will be filtered out for as long as that thread (or more likely, your app which creates that thread) is running.

There's a boilerplate example in the if __name__ == '__main__' block so you can try running it directly to see how it works.

import atexit
import ctypes
import win32api
import win32con
import win32gui
from enum import Enum


class ForbiddenKeys(int, Enum):
    """Enumerator of keycodes that will be filtered out"""
    # NOTE: inheriting from int allows for testing of membership once the Enum
    # is cast to a set, e.g.: 'if <val> in set(ForbiddenKeys)'
    # (int class must come before Enum)
    KEY_LWIN = win32con.VK_LWIN  # 0x5B
    KEY_RWIN = win32con.VK_RWIN  # 0x5C
    ...
    # you can add more keys here as needed
    # e.g.: KEY_LCTRL = 0xA2


class KeyFilter:
    """
    Sets up a Windows low-level keyboard hook to filter out "forbidden" keys:
    (keycodes defined in ForbiddenKeys enum)
    """
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    CMPFUNC = ctypes.CFUNCTYPE(
        ctypes.c_int,
        ctypes.c_int,
        ctypes.c_int,
        ctypes.POINTER(ctypes.c_void_p)
    )

    def _filter_handler(self, nCode, wParam, lParam) -> int:
        keycode = lParam[0] & 0xFFFF_FFFF  # upper bits of keycode are masked
        if keycode in set(ForbiddenKeys):
            return 1
        else:  # key allowed, move along to the next hook
            return self.user32.CallNextHookEx(
                self.hook_id,
                nCode,
                wParam,
                lParam
            )

    def _unhook(self) -> None:
        """Unhook the keyboard hook and stop the listener loop"""
        self.user32.UnhookWindowsHookEx(self.hook_id)

    def set_hook(self) -> None:
        """Listen for keyboard events until unhooked at exit"""
        handler_ptr = self.CMPFUNC(self._filter_handler)
        # NOTE: argtypes are required for 64-bit Python compatibility
        self.user32.SetWindowsHookExW.argtypes = (
            ctypes.c_int,
            ctypes.c_void_p,
            ctypes.c_void_p,
            ctypes.c_uint,
        )
        # set up a Windows low-level keyboard hook
        self.hook_id = self.user32.SetWindowsHookExW(
            win32con.WH_KEYBOARD_LL,  # hook type: low-level keyboard input
            handler_ptr,  # point to the handler method
            win32api.GetModuleHandle(None),  # handler is in this module
            0,
        )
        atexit.register(self._unhook)  # release hook at interpreter exit
        # start the message loop
        while (msg := win32gui.GetMessage(0, 0, 0)) > 0:
            win32gui.TranslateMessage(ctypes.byref(msg))
            win32gui.DispatchMessage(ctypes.byref(msg))


if __name__ == '__main__':
    # test keyfilter
    from threading import Thread

    keyfilter = KeyFilter()
    keyfilter_thread = Thread(
        target=keyfilter.set_hook,
        name='KeyFilter',
        daemon=True,
    )
    keyfilter_thread.start()
    # press ctrl-c or close the terminal to stop the keyfilter
    while True:
        try:
            pass
        except KeyboardInterrupt:
            break