I’m setting up a hotkey system for Windows in Python, using the Win32 API and PySide6. I want to register hotkeys in a HotkeyManager class and listen for them in a separate thread, so the GUI remains responsive. However, when I move the listening logic to a thread, the hotkey events are not detected correctly.
Here’s the code that works without using threads, where hotkeys are registered and detected on the main thread:
from threading import Thread
from typing import Callable, Dict
from win32gui import RegisterHotKey, UnregisterHotKey, GetMessage
from win32con import VK_NUMPAD0, MOD_NOREPEAT
class HotkeyManager:
def __init__(self):
self.hotkey_id = 1
self._callbacks: Dict[int, Callable] = {}
def register_hotkey(self, key_code: int, callback: Callable):
self._callbacks[self.hotkey_id] = callback
RegisterHotKey(0, self.hotkey_id, MOD_NOREPEAT, key_code)
self.hotkey_id += 1
def listen(self):
while True:
print("Listener started.")
msg = GetMessage(None, 0, 0)
hotkey_id = msg[1]
if hotkey_id in self._callbacks:
self._callbacks[hotkey_id]()
In the main code, this setup works as expected:
from PySide6 import QtWidgets
from win32con import VK_NUMPAD0
def on_press():
print("Numpad 0 pressed!")
app = QtWidgets.QApplication([])
manager = HotkeyManager()
manager.register_hotkey(VK_NUMPAD0, on_press)
manager.listen()
# Initialize window
widget = QtWidgets.QMainWindow()
widget.show()
app.exec()
When I try to move the listen() method to a separate thread, however, the hotkey doesn’t respond properly:
class HotkeyManager:
def listen(self):
def run():
while True:
print("Listener started.")
msg = GetMessage(None, 0, 0)
hotkey_id = msg[1]
if hotkey_id in self._callbacks:
self._callbacks[hotkey_id]()
thread = Thread(target=run, daemon=True)
thread.start()
How can I correctly listen for hotkeys in a separate thread without losing functionality? It seems that the issue may be due to the hotkeys being registered on the main thread while the listening logic runs in a secondary thread. How could I solve this so everything works as expected?
Regarding the statement: "Here’s the code that works without using threads", there's nothing about code in the question that actually works. Let me detail:
from win32con import VK_NUMPAD0, MOD_NOREPEAT
would yield ImportError. Check [GitHub]: mhammond/pywin32 - feat: add MOD_NOREPEAT (RegisterHotKey) constant (that I submitted a few minutes ago)
[GitHub.MHammond]: win32gui.GetMessage (documentation) seems to be wrong, the return is a tuple of 2 elements:
Underlying GetMessage return code (BOOL)
Submitted [GitHub]: mhammond/pywin32 - win32gui.GetMessage documentation is incorrect for this
This way of having a thread monitoring for WM_HOTKEY, seems a bit clumsy as the window has its own message loop, and if it was for me to chose, I'd try to handle these messages in the window's message processing function, but a shallow Google search didn't reveal anything useful.
Posting a solution that works based on your code. The idea is to wrap the hotkey registering and listening in a function to be executed in a thread.
code00.py:
#!/usr/bin/env python
import sys
import threading
import typing
import win32con as wcon
import win32gui as wgui
from PySide6 import QtWidgets
MOD_NOREPEAT = 0x4000
class HotkeyManager:
def __init__(self):
self.hotkey_id = 1
self._callbacks: typing.Dict[int, typing.Callable] = {}
self.threads = []
def __del__(self):
self.clear()
def register_hotkey(self, key_code: int, callback: typing.Callable):
t = threading.Thread(target=self._register_hotkey, args=(key_code, callback), daemon=True)
self.threads.append(t)
t.start()
def _register_hotkey(self, key_code: int, callback: typing.Callable):
self._callbacks[self.hotkey_id] = callback
wgui.RegisterHotKey(None, self.hotkey_id, MOD_NOREPEAT, key_code)
self.hotkey_id += 1
self._listen()
def _listen(self):
print(f"Listener started ({threading.get_ident()}, {threading.get_native_id()})")
while True:
res = wgui.GetMessage(None, 0, 0)
print(f" GetMessage returned: {res}") # @TODO - cfati: Check what it returns!
rc, msg = res
hotkey_id = msg[2]
if hotkey_id in self._callbacks:
self._callbacks[hotkey_id]()
def clear(self):
for hkid in self._callbacks:
wgui.UnregisterHotKey(None, hkid)
self._callbacks = {}
def on_press_np0():
print(f"NumPad0 pressed!")
def main(*argv):
print(f"Main thread ({threading.get_ident()}, {threading.get_native_id()})")
app = QtWidgets.QApplication([])
widget = QtWidgets.QMainWindow()
widget.setWindowTitle("SO q079161068")
widget.show()
print("HotKeys")
hkm = HotkeyManager()
hkm.register_hotkey(wcon.VK_NUMPAD0, on_press_np0)
print("App mesage loop")
app.exec()
if __name__ == "__main__":
print(
"Python {:s} {:03d}bit on {:s}\n".format(
" ".join(elem.strip() for elem in sys.version.split("\n")),
64 if sys.maxsize > 0x100000000 else 32,
sys.platform,
)
)
rc = main(*sys.argv[1:])
print("\nDone.\n")
sys.exit(rc)
Output:
[cfati@CFATI-5510-0:e:\Work\Dev\StackExchange\StackOverflow\q079161068]> "e:\Work\Dev\VEnvs\py_pc064_03.10_test0\Scripts\python.exe" ./code00.py Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr 5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] 064bit on win32 Main thread (23772, 23772) HotKeys App mesage loop Listener started (23672, 23672) GetMessage returned: [1, (0, 786, 1, 6291456, 514232828, (1, 0))] NumPad0 pressed! GetMessage returned: [1, (0, 786, 1, 6291456, 514234437, (1919, 0))] NumPad0 pressed! GetMessage returned: [1, (0, 786, 1, 6291456, 514235703, (1919, 1079))] NumPad0 pressed! GetMessage returned: [1, (0, 786, 1, 6291456, 514237421, (0, 1079))] NumPad0 pressed! GetMessage returned: [1, (0, 786, 1, 6291456, 514238718, (933, 534))] NumPad0 pressed! GetMessage returned: [1, (0, 786, 1, 6291456, 514244375, (1650, 301))] NumPad0 pressed! Done.
Also posting a screenshot:
As a note, while thread is running NumPad 0 keystroke doesn't reach to other windows.