Search code examples
pythonpyqt5keyboard-eventskeylogger

Detect external keyboard events in PyQt5


How can I implement a key listener in PyQT5? I want to detect keypresses even when the app is in background.

from PyQt5 import QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt
import sys


class Window(QWidget):
    
    ...
       

    def keyPressEvent(self, e): # doesnt work when app is in background
        if e.key() == Qt.Key_F3:
            print(1)
        elif e.key() == Qt.Key_F4:
            print(0)

   ...

        
App = QApplication(sys.argv)
App.setStyle('Fusion')
window = Window()
sys.exit(App.exec())



Solution

  • Qt can access keyboard events only if any of its top level window has keyboard focus. If the window is minimized or another window takes focus, you will not receive keyboard events.

    The only solution is to use an external library, but they have limitations.

    The keyboard module does not seem to support macOS, while pyinput does, but requires root access for that OS. I don't know of any other ways that support all three platforms without limitations.

    In any case, you should not rely on timed checking of the currently pressed keys, because you'll certainly end up missing some events.
    While normally one would use a separate thread that implements the event listener (which are normally blocking), luckily in both cases there are non blocking systems to call callback functions (so you don't actually need a separate thread).

    The following is a basic example using the keyboard module:

    from PyQt5 import QtCore, QtWidgets
    import keyboard
    
    class KeyGrabber(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            layout = QtWidgets.QVBoxLayout(self)
            self.button = QtWidgets.QPushButton('start')
            layout.addWidget(self.button)
            self.button.setCheckable(True)
            self.button.toggled.connect(self.setGrabbing)
    
        def keyboardEventReceived(self, event):
            if event.event_type == 'down':
                if event.name == 'f3':
                    print('F3 pressed')
                elif event.name == 'f4':
                    print('F4 pressed')
    
        def setGrabbing(self, enable):
            if enable:
                self.button.setText('stop')
                # on_press returns a hook that can be used to "disconnect" the callback
                # function later, if required
                self.hook = keyboard.on_press(self.keyboardEventReceived)
                self.showMinimized()
            else:
                self.button.setText('start')
                keyboard.unhook(self.hook)