pythonpyqtkeyboardpyqt6intercept

Key code results inconsistent when typing on the numpad


I decided to make a primitive program to intercept hotkeys and output them to the screen to create tutorial videos. Currently, everything works as I wanted, except for Numpad numbers and Numpad math symbols (Numpad "0" and "." don't display correctly either). Help bring the program to completion. It is desirable that, like the rest of the keys, the Numpad keys are correctly displayed both independently and with all combinations of Ctrl, Shift and Alt keys.

from PyQt6 import QtWidgets, QtCore
from PyQt6.QtGui import QFont
from pynput import keyboard, mouse


class KeyLoggerApp(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.ctrl_pressed = False

        self.setMinimumSize(400, 300)
        font = QFont()
        font.setPointSize(18)

        self.layout = QtWidgets.QVBoxLayout()
        self.ctrl_label = QtWidgets.QLabel(self)
        self.ctrl_label.setFont(font)
        self.shift_label = QtWidgets.QLabel(self)
        self.shift_label.setFont(font)
        self.alt_label = QtWidgets.QLabel(self)
        self.alt_label.setFont(font)
        self.mouse_left_label = QtWidgets.QLabel(self)
        self.mouse_left_label.setFont(font)
        self.mouse_middle_label = QtWidgets.QLabel(self)
        self.mouse_middle_label.setFont(font)
        self.mouse_right_label = QtWidgets.QLabel(self)
        self.mouse_right_label.setFont(font)
        self.key_label = QtWidgets.QLabel(self)
        self.key_label.setFont(font)

        self.layout.addWidget(self.ctrl_label)
        self.layout.addWidget(self.shift_label)
        self.layout.addWidget(self.alt_label)
        self.layout.addWidget(self.mouse_left_label)
        self.layout.addWidget(self.mouse_middle_label)
        self.layout.addWidget(self.mouse_right_label)
        self.layout.addWidget(self.key_label)

        self.central_widget = QtWidgets.QWidget()
        self.central_widget.setLayout(self.layout)
        self.setCentralWidget(self.central_widget)

        self.toggle_button = QtWidgets.QPushButton("Toggle Always on Top", self)
        self.toggle_button.clicked.connect(self.toggle_on_top)
        self.addToolBar(QtCore.Qt.ToolBarArea.TopToolBarArea, self.create_toolbar())


        self.key_listener = keyboard.Listener(
            on_press=self.on_key_press,
            on_release=self.on_key_release)
        self.key_listener.start()

        self.mouse_listener = mouse.Listener(
            on_click=self.on_mouse_click,
            on_release=self.on_mouse_release)
        self.mouse_listener.start()

    def create_toolbar(self):
        toolbar = QtWidgets.QToolBar()
        toolbar.addWidget(self.toggle_button)
        return toolbar

    def toggle_on_top(self):
        if self.windowFlags() & QtCore.Qt.WindowType.WindowStaysOnTopHint:
            self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowStaysOnTopHint)
        else:
            self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowType.WindowStaysOnTopHint)
        self.show()

    def on_key_press(self, key):
        if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
            self.ctrl_label.setText('Ctrl pressed')
        elif key == keyboard.Key.shift:
            self.shift_label.setText('Shift pressed')
        elif key == keyboard.Key.alt_l or key == keyboard.Key.alt_r:
            self.alt_label.setText('Alt pressed')
        elif key == keyboard.Key.space:  # Додано обробку клавіші пробілу
            self.key_label.setText('Space pressed')  # Ви можете змінити текст на свій розсуд
        else:
            try:
                if isinstance(key, keyboard.KeyCode):
                    if key.char is not None and ord(key.char) < 32:  # check if the key is a control character
                        self.key_label.setText("'{0}' pressed".format(chr(ord(key.char) + 64).upper()))
                    elif (key.vk >= 48 and key.vk <= 57) or (key.vk >= 65 and key.vk <= 90) or (
                            key.vk >= 97 and key.vk <= 122):  # check if the key is a digit or a letter
                        self.key_label.setText("'{0}' pressed".format(chr(key.vk)))
                    else:
                        # Convert to English layout
                        key_char = self.convert_to_english_layout(key.char)
                        self.key_label.setText("alphanumeric key '{0}' pressed".format(key_char.upper()))
            except AttributeError:
                self.key_label.setText("special key '{0}' pressed".format(str(key).upper()))


    def on_key_release(self, key):
        if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
            self.ctrl_label.clear()
        elif key == keyboard.Key.shift:
            self.shift_label.clear()
        elif key == keyboard.Key.alt_l or key == keyboard.Key.alt_r:
            self.alt_label.clear()
        else:
            if isinstance(key, keyboard.KeyCode):
                if key.char is not None and ord(key.char) < 32:
                    self.key_label.setText("{0} released".format(str(chr(ord(key.char) + 64).upper())))
                elif (key.vk >= 48 and key.vk <= 57) or (key.vk >= 65 and key.vk <= 90) or (
                        key.vk >= 97 and key.vk <= 122):  # check if the key is a digit or a letter
                    self.key_label.setText("{0} released".format(chr(key.vk)))
                else:
                    # Convert to English layout
                    key_char = self.convert_to_english_layout(key.char)
                    if key_char is not None:
                        self.key_label.setText("{0} released".format(key_char.upper()))
                    else:
                        self.key_label.setText("None released")
            elif key == keyboard.Key.space:  # Додано обробку клавіші пробілу
                self.key_label.setText('Space released')  # Ви можете змінити текст на свій розсуд
            else:
                self.key_label.setText("{0} released".format(str(key).upper()))

    def convert_to_english_layout(self, char):
        # Define a dictionary to map Ukrainian characters to English
        uk_to_en = {
            'й': 'q', 'ц': 'w', 'у': 'e', 'к': 'r', 'е': 't', 'н': 'y', 'г': 'u', 'ш': 'i', 'щ': 'o', 'з': 'p',
            'х': '[', 'ї': ']', 'ф': 'a', 'і': 's', 'в': 'd', 'а': 'f', 'п': 'g', 'р': 'h', 'о': 'j', 'л': 'k',
            'д': 'l', 'ж': ';', 'є': '\'', 'я': 'z', 'ч': 'x', 'с': 'c', 'м': 'v', 'и': 'b', 'т': 'n', 'ь': 'm',
            'б': ',', 'ю': '.', 'ґ': '`'
        }

        # Convert the character to lower case
        if char is not None:
            char = char.lower()

        # Check if the character is in the dictionary
        if char in uk_to_en:
            return uk_to_en[char]
        else:
            return char

    def on_mouse_click(self, x, y, button, pressed):
        if button == mouse.Button.left:
            self.mouse_left_label.setText('Mouse left button pressed' if pressed else '')
        elif button == mouse.Button.middle:
            self.mouse_middle_label.setText('Mouse middle button pressed' if pressed else '')
        elif button == mouse.Button.right:
            self.mouse_right_label.setText('Mouse right button pressed' if pressed else '')

    def on_mouse_release(self, x, y, button):
        print('mouse')
        if button == mouse.Button.left:
            self.mouse_left_label.clear()
        elif button == mouse.Button.middle:
            self.mouse_middle_label.clear()
        elif button == mouse.Button.right:
            self.mouse_right_label.clear()

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = KeyLoggerApp()
    window.show()
    app.exec()

UPD: When I press the keys on the Numpad, the interceptor displays letters instead: "1" = "a", "2" = "b", "3" = "c", "4" = "d", " 5" = "e", "6" = "f", "7" = "g", "8" = "h", "9" = "i" . And instead of mathematical symbols, Numpad also displays letters: "/" = "o", "*" = "j", "-" = "m", "+" = "k", "." = "n" operating system - Windows 11. The keyboard I use: enter image description here


Solution

  • You are confusing the key codes with ascii codes.

    While some of them actually coincide (notably, numbers and capitalized letters), they cannot be used as some sort of reference conversion. The similarity of those key codes is only because, for logical reasons, it made more sense to use the known existing values (the ASCII table).

    By following a virtual key code table (such as this), you can easily find that the codes for numeric pad keys actually match your wrong output: for instance, 1 on the numeric pad corresponds to the virtual key 97, which gives you a if you try to use it with chr().

    So, the solution is simple: do not use chr() for that; it's better to create a dictionary with the known mapping of letters and keys, possibly as a global constant, and eventually use a fall back output of the vk and char for unexpected keys.