Странное переключение раскладки

Сделал тестовое переключение раскладки:

import win32api
import pyautogui
lcid = win32api.GetKeyboardLayout()
lang_code = hex(lcid & 0xFFFF)
print(lang_code)
pyautogui.hotkey('shift', 'alt')
lcid = win32api.GetKeyboardLayout()
lang_code = hex(lcid & 0xFFFF)
print(lang_code)

Но работает это очень странно: раскладка переключается, но Pyhton этого не замечает.

В print(lang_code) и до и после переключения он выводит одно и то же значение. При повторном запуске значение меняется, но всё равно остается одним и тем же, как до, так и после переключения.

В чём тут может быть дело?


Ответы (2 шт):

Автор решения: Amgarak

Почитав тему из комментариев выше, пошёл изучать enSO и нашел интересное обсуждение.

Выводы:

Операционная система отправляет сообщение WM_INPUTLANGCHANGE только в "верхнее активное окно", уведомляя его об изменениях языка. Если приложение не активно, вызов метода GetKeyboardLayout всегда будет возвращать последнее известное значение языка, которое может быть устаревшим.

Собственно, решать проблему предлагают везде одинакова:

Извлечь язык клавиатуры, используя текущий активный оконный процесс.

Минимальный пример:

import time
import ctypes

def get_keyboard_layout():
    
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    # Дескриптор активного окна
    hwnd = user32.GetForegroundWindow()
    # Идентификатор потока из дескриптора окна
    thread_id = user32.GetWindowThreadProcessId(hwnd, None)
    # Идентификатор раскладки клавиатуры из thread_id
    layout_id = user32.GetKeyboardLayout(thread_id)
    # Язык клавиатуры из идентификатора раскладки клавиатуры
    lang_id = layout_id & 0xFFFF
    return "RU" if lang_id == 0x0419 else "EN"

if __name__ == "__main__":
    while True:
        current_layout = get_keyboard_layout()
        print(f'Текущая раскладка клавиатуры: {current_layout}')
        time.sleep(1)

Немного позаимствовал идею с MessageBox и Language IDs сгруппировав это всё в один пример:

import time
import ctypes
import threading
from tkinter import messagebox, Tk

class KeyboardLayoutWatcher:
    languages = {'0x436' : "Afrikaans - South Africa", '0x041c' : "Albanian - Albania", '0x045e' : "Amharic - Ethiopia", '0x401' : "Arabic - Saudi Arabia",
                 '0x1401' : "Arabic - Algeria", '0x3c01' : "Arabic - Bahrain", '0x0c01' : "Arabic - Egypt", '0x801' : "Arabic - Iraq", '0x2c01' : "Arabic - Jordan",
                 '0x3401' : "Arabic - Kuwait", '0x3001' : "Arabic - Lebanon", '0x1001' : "Arabic - Libya", '0x1801' : "Arabic - Morocco", '0x2001' : "Arabic - Oman",
                 '0x4001' : "Arabic - Qatar", '0x2801' : "Arabic - Syria", '0x1c01' : "Arabic - Tunisia", '0x3801' : "Arabic - U.A.E.", '0x2401' : "Arabic - Yemen",
                 '0x042b' : "Armenian - Armenia", '0x044d' : "Assamese", '0x082c' : "Azeri (Cyrillic)", '0x042c' : "Azeri (Latin)", '0x042d' : "Basque",
                 '0x423' : "Belarusian", '0x445' : "Bengali (India)", '0x845' : "Bengali (Bangladesh)", '0x141A' : "Bosnian (Bosnia/Herzegovina)", '0x402' : "Bulgarian",
                 '0x455' : "Burmese", '0x403' : "Catalan", '0x045c' : "Cherokee - United States", '0x804' : "Chinese - People's Republic of China", 
                 '0x1004' : "Chinese - Singapore", '0x404' : "Chinese - Taiwan", '0x0c04' : "Chinese - Hong Kong SAR", '0x1404' : "Chinese - Macao SAR", '0x041a' : "Croatian",
                 '0x101a' : "Croatian (Bosnia/Herzegovina)", '0x405' : "Czech", '0x406' : "Danish", '0x465' : "Divehi", '0x413' : "Dutch - Netherlands", '0x813' : "Dutch - Belgium",
                 '0x466' : "Edo", '0x409' : "English - United States", '0x809' : "English - United Kingdom", '0x0c09' : "English - Australia", '0x2809' : "English - Belize",
                 '0x1009' : "English - Canada", '0x2409' : "English - Caribbean", '0x3c09' : "English - Hong Kong SAR", '0x4009' : "English - India", '0x3809' : "English - Indonesia",
                 '0x1809' : "English - Ireland", '0x2009' : "English - Jamaica", '0x4409' : "English - Malaysia", '0x1409' : "English - New Zealand", '0x3409' : "English - Philippines",
                 '0x4809' : "English - Singapore", '0x1c09' : "English - South Africa", '0x2c09' : "English - Trinidad", '0x3009' : "English - Zimbabwe", '0x425' : "Estonian",
                 '0x438' : "Faroese", '0x429' : "Farsi", '0x464' : "Filipino", '0x040b' : "Finnish", '0x040c' : "French - France", '0x080c' : "French - Belgium",
                 '0x2c0c' : "French - Cameroon", '0x0c0c' : "French - Canada", '0x240c' : "French - Democratic Rep. of Congo", '0x300c' : "French - Cote d'Ivoire",
                 '0x3c0c' : "French - Haiti", '0x140c' : "French - Luxembourg", '0x340c' : "French - Mali", '0x180c' : "French - Monaco", '0x380c' : "French - Morocco",
                 '0xe40c' : "French - North Africa", '0x200c' : "French - Reunion", '0x280c' : "French - Senegal", '0x100c' : "French - Switzerland", 
                 '0x1c0c' : "French - West Indies", '0x462' : "Frisian - Netherlands", '0x467' : "Fulfulde - Nigeria", '0x042f' : "FYRO Macedonian", '0x083c' : "Gaelic (Ireland)",
                 '0x043c' : "Gaelic (Scotland)", '0x456' : "Galician", '0x437' : "Georgian", '0x407' : "German - Germany", '0x0c07' : "German - Austria", '0x1407' : "German - Liechtenstein",
                 '0x1007' : "German - Luxembourg", '0x807' : "German - Switzerland", '0x408' : "Greek", '0x474' : "Guarani - Paraguay", '0x447' : "Gujarati", '0x468' : "Hausa - Nigeria",
                 '0x475' : "Hawaiian - United States", '0x040d' : "Hebrew", '0x439' : "Hindi", '0x040e' : "Hungarian", '0x469' : "Ibibio - Nigeria", '0x040f' : "Icelandic",
                 '0x470' : "Igbo - Nigeria", '0x421' : "Indonesian", '0x045d' : "Inuktitut", '0x410' : "Italian - Italy", '0x810' : "Italian - Switzerland", '0x411' : "Japanese",
                 '0x044b' : "Kannada", '0x471' : "Kanuri - Nigeria", '0x860' : "Kashmiri", '0x460' : "Kashmiri (Arabic)", '0x043f' : "Kazakh", '0x453' : "Khmer", '0x457' : "Konkani",
                 '0x412' : "Korean", '0x440' : "Kyrgyz (Cyrillic)", '0x454' : "Lao", '0x476' : "Latin", '0x426' : "Latvian", '0x427' : "Lithuanian", '0x043e' : "Malay - Malaysia",
                 '0x083e' : "Malay - Brunei Darussalam", '0x044c' : "Malayalam", '0x043a' : "Maltese", '0x458' : "Manipuri", '0x481' : "Maori - New Zealand", '0x044e' : "Marathi",
                 '0x450' : "Mongolian (Cyrillic)", '0x850' : "Mongolian (Mongolian)", '0x461' : "Nepali", '0x861' : "Nepali - India", '0x414' : "Norwegian (Bokmål)", 
                 '0x814' : "Norwegian (Nynorsk)", '0x448' : "Oriya", '0x472' : "Oromo", '0x479' : "Papiamentu", '0x463' : "Pashto", '0x415' : "Polish", '0x416' : "Portuguese - Brazil",
                 '0x816' : "Portuguese - Portugal", '0x446' : "Punjabi", '0x846' : "Punjabi (Pakistan)", '0x046B' : "Quecha - Bolivia", '0x086B' : "Quecha - Ecuador", 
                 '0x0C6B' : "Quecha - Peru", '0x417' : "Rhaeto-Romanic", '0x418' : "Romanian", '0x818' : "Romanian - Moldava", '0x419' : "Russian", '0x819' : "Russian - Moldava",
                 '0x043b' : "Sami (Lappish)", '0x044f' : "Sanskrit", '0x046c' : "Sepedi", '0x0c1a' : "Serbian (Cyrillic)", '0x081a' : "Serbian (Latin)", '0x459' : "Sindhi - India",
                 '0x859' : "Sindhi - Pakistan", '0x045b' : "Sinhalese - Sri Lanka", '0x041b' : "Slovak", '0x424' : "Slovenian", '0x477' : "Somali", '0x042e' : "Sorbian", 
                 '0x0c0a' : "Spanish - Spain (Modern Sort)", '0x040a' : "Spanish - Spain (Traditional Sort)", '0x2c0a' : "Spanish - Argentina", '0x400a' : "Spanish - Bolivia",
                 '0x340a' : "Spanish - Chile", '0x240a' : "Spanish - Colombia", '0x140a' : "Spanish - Costa Rica", '0x1c0a' : "Spanish - Dominican Republic", 
                 '0x300a' : "Spanish - Ecuador", '0x440a' : "Spanish - El Salvador", '0x100a' : "Spanish - Guatemala", '0x480a' : "Spanish - Honduras", '0xe40a' : "Spanish - Latin America",
                 '0x080a' : "Spanish - Mexico", '0x4c0a' : "Spanish - Nicaragua", '0x180a' : "Spanish - Panama", '0x3c0a' : "Spanish - Paraguay", '0x280a' : "Spanish - Peru",
                 '0x500a' : "Spanish - Puerto Rico", '0x540a' : "Spanish - United States", '0x380a' : "Spanish - Uruguay", '0x200a' : "Spanish - Venezuela", '0x430' : "Sutu",
                 '0x441' : "Swahili", '0x041d' : "Swedish", '0x081d' : "Swedish - Finland", '0x045a' : "Syriac", '0x428' : "Tajik", '0x045f' : "Tamazight (Arabic)", 
                 '0x085f' : "Tamazight (Latin)", '0x449' : "Tamil", '0x444' : "Tatar", '0x044a' : "Telugu", '0x041e' : "Thai", '0x851' : "Tibetan - Bhutan", 
                 '0x451' : "Tibetan - People's Republic of China", '0x873' : "Tigrigna - Eritrea", '0x473' : "Tigrigna - Ethiopia", '0x431' : "Tsonga", '0x432' : "Tswana",
                 '0x041f' : "Turkish", '0x442' : "Turkmen", '0x480' : "Uighur - China", '0x422' : "Ukrainian", '0x420' : "Urdu", '0x820' : "Urdu - India", '0x843' : "Uzbek (Cyrillic)",
                 '0x443' : "Uzbek (Latin)", '0x433' : "Venda", '0x042a' : "Vietnamese", '0x452' : "Welsh", '0x434' : "Xhosa", '0x478' : "Yi", '0x043d' : "Yiddish", '0x046a' : "Yoruba",
                 '0x435' : "Zulu", '0x04ff' : "HID (Human Interface Device)"
                 }

    def __init__(self):
        self.current_language = None
        self.root = Tk()  
        self.root.withdraw()  
        self.root.attributes('-topmost', True) # Поверх других окон для наглядности

    def get_keyboard_layout(self):
        user32 = ctypes.WinDLL('user32', use_last_error=True)
        hwnd = user32.GetForegroundWindow()
        thread_id = user32.GetWindowThreadProcessId(hwnd, None)
        layout_id  = user32.GetKeyboardLayout(thread_id)
        lang_id = layout_id  & 0xFFFF # lang_id  = layout_id & (2 ** 16 - 1)
        language_id_hex = hex(lang_id) 

        if language_id_hex in self.languages.keys():
            return self.languages[language_id_hex]
        else:
            return str(language_id_hex)

    def handle_current_language(self):
        new_language = self.get_keyboard_layout()
        if self.current_language != new_language:
            self.current_language = new_language
            messagebox.showinfo("Изменение раскладки", f"Текущая раскладка клавиатуры: {self.current_language}")

    def watch_language(self):
        while True:
            self.handle_current_language()
            time.sleep(0.5)

    def start(self):
        threading.Thread(target=self.watch_language, daemon=True).start()
        self.root.mainloop()  

if __name__ == "__main__":
    watcher = KeyboardLayoutWatcher()
    watcher.start()

введите сюда описание изображения

Немного разочаровывает, что в Windows нет АПИ для получения текущей раскладки(глобальной) и нужно колхозить через окно в фокусе. Но если немного разобраться, проблема становиться очевидной.


import time
import ctypes

def get_keyboard_layout():
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    layout_id = user32.GetKeyboardLayout(0)
    lang_id = layout_id & 0xFFFF
    return "RU" if lang_id == 0x0419 else "EN"
    

if __name__ == "__main__":
    while True:
        current_layout = get_keyboard_layout()
        print(f'Текущая раскладка клавиатуры: {current_layout}')
        time.sleep(1)

Может возникнуть вопрос: почему данный код при запуске в консольной программе всегда будет возвращать одну и туже раскладку(стартовую), даже если раскладку поменять?

Ответ: Проблема в том, что консольное приложение не позволяет обрабатывать сообщения Windows.

Так что придётся создавать своё окно и уже там обрабатывать сообщения WM_INPUTLANGCHANGE, которое срабатывает при изменении раскладки клавиатуры - по найденным материалам --один--, --два-- получился пример:

import ctypes
import win32gui
import win32con
import win32api

def get_keyboard_layout():
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    layout_id = user32.GetKeyboardLayout(0)
    lang_id = layout_id & 0xFFFF
    return "RU" if lang_id == 0x0419 else "EN"

class MyWindow:
    def __init__(self):
        wc = win32gui.WNDCLASS()
        wc.lpfnWndProc = self.wnd_proc  
        wc.lpszClassName = "MyWindowClass"
        wc.hInstance = win32api.GetModuleHandle(None)
        self.class_atom = win32gui.RegisterClass(wc)
        
        self.hwnd = win32gui.CreateWindow(
            self.class_atom,
            "Current language",
            win32con.WS_OVERLAPPEDWINDOW, 
            100, 100, 200, 100,  
            0, 0, wc.hInstance, None
        )
        
        self.h_textbox = win32gui.CreateWindowEx(
            0,
            "EDIT",
            "",
            win32con.WS_CHILD | win32con.WS_VISIBLE | win32con.WS_BORDER | win32con.ES_MULTILINE,
            10, 10, 150, 50,  
            self.hwnd,
            0,
            wc.hInstance,
            None
        )

        self.current_layout = get_keyboard_layout()  
        self.update_textbox()  

    def update_textbox(self):
        win32gui.SetWindowText(self.h_textbox, f"Язык ввода: {self.current_layout}")
        print(f"Язык ввода {self.current_layout}")

    def wnd_proc(self, hwnd, msg, wparam, lparam):
        if msg == win32con.WM_INPUTLANGCHANGE:
            self.current_layout = get_keyboard_layout()
            self.update_textbox()  
        return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)

    def run(self):
        win32gui.ShowWindow(self.hwnd, win32con.SW_SHOW)
        win32gui.UpdateWindow(self.hwnd)
        while True:
            win32gui.PumpWaitingMessages()  

if __name__ == "__main__":
    window = MyWindow()
    window.run()

Но опять таки, даже так раскладка будет изменятся только при фокусе (или возвращении фокуса) на нашем окне.


P.S. Возможно я что-то упустил или не правильно понял, не стесняйтесь поправить в комментариях и я дополню ответ.

→ Ссылка
Автор решения: Fox Fox

Как определить текущую раскладку и если она неанглийская переключить на неё? Вот готовый консольный скрипт (для Windows; обращаю внимание, что после выхода из программы Windows вернёт назад ту раскладку, которая была в её среде):

import os
import ctypes
import keyboard

print("-" * 50 + "\nПереключение текущей раскладки клавиатуры на английскую:\n" + "-" * 50)

while ctypes.WinDLL("user32", use_last_error=True).GetKeyboardLayout(0) & 0xFFFF != 1033:
 keyboard.send("ctrl+shift") # или keyboard.send("alt+shift")

os.system("pause")
→ Ссылка