НейроАгент

Исправить AHK-скрипт переключения раскладки в системных окнах

Решение проблемы с AHK-скриптом переключения раскладки клавиатуры в системных диалогах Windows. Узнайте, почему SendMessage не работает и как исправить скрипт для работы в окнах сохранения файлов и других системных диалогах.

Вопрос

Как исправить AHK-скрипт для переключения раскладки клавиатуры в системных окнах Windows?

Я использую Windows 10 и впервые работаю с AutoHotkey. На основе опыта работы с Linux я создал скрипт для переключения раскладки: английская при отпускании левого Ctrl, русская при отпускании правого Ctrl.

Вот мой скрипт:

~LCtrl up::
    If (A_PriorKey = "LControl") {
        SendMessage, 0x50,, 0x4090409,, A ; английский
    }
    
~RCtrl up::
    If (A_PriorKey = "RControl") {
        SendMessage, 0x50,, 0x4190419,, A ; русский
    }

Скрипт работает корректно в обычных окнах, но не срабатывает в системных окнах Windows, например, в стандартном диалоговом окне сохранения файла.

В чем заключается проблема и как ее исправить?

Примечание: Я знаю, что PuntoSwitcher решает эту задачу, но предпочитаю не использовать эту программу. Моя цель - оставить в системе единственный способ переключения языков через Ctrl, без добавления дополнительных комбинаций клавиш в системных настройках и эмуляции их нажатия.

НейроАгент

SendMessage с кодом 0x50 (WM_INPUTLANGCHANGEREQUEST) не работает в системных диалогах Windows, таких как окно сохранения файла, потому что эти окна имеют особую обработку сообщений и не всегда реагируют на стандартные сообщения переключения раскладки. Системные диалоги часто используют собственный механизм обработки ввода, который игнорирует SendMessage.


Содержание


Основные проблемы SendMessage в системных диалогах

SendMessage с кодом 0x50 (WM_INPUTLANGCHANGEREQUEST) не работает в системных диалогах по нескольким причинам:

  1. Особая обработка сообщений: Системные диалоги Windows (сохранение файла, поиск, свойства и т.д.) имеют собственный механизм обработки ввода, который может игнорировать стандартные сообщения переключения раскладки.

  2. Ограничения безопасности: Многие системные окна работают в повышенном контексте безопасности и не принимают сообщения от внешних приложений.

  3. Фокус ввода: Системные диалоги могут не обновлять свое внутреннее состояние раскладки клавиатуры при получении SendMessage.

Как указывает AutoHotkey Community, SendMessage/PostMessage не работает, если все окна минимизированы, и это распространенная проблема.


Альтернативные методы переключения раскладки

1. Использование Windows API и IMM

Один из самых надежных методов - использование прямых вызовов Windows API через DllCall. Этот метод использует Input Method Manager (IMM) для переключения раскладки.

autohotkey
~LCtrl up::
    If (A_PriorKey = "LControl") {
        SwitchToLayout(0x0409) ; Английский (США)
    }
return

~RCtrl up::
    If (A_PriorKey = "RControl") {
        SwitchToLayout(0x0419) ; Русский
    }
return

SwitchToLayout(layoutID) {
    static HKL := A_PtrSize = 8 ? "Ptr" : "UInt"
    
    ; Получаем текущий язык ввода
    hkl := DllCall("GetKeyboardLayout", "UInt", 0, HKL)
    
    ; Получаем список установленных языков
    cb := 32
    cch := 256
    p := DllCall("GlobalAlloc", "UInt", 0x40, "UInt", cb * cch, "UPtr")
    
    VarSetCapacity(buf, cb * cch)
    DllCall("GetKeyboardLayoutList", "Int", cb, "UPtr", p)
    
    ; Ищем нужный язык
    found := false
    loop cb {
        hkl_current := NumGet(p + (A_Index - 1) * A_PtrSize, 0, HKL)
        if (hkl_current = layoutID) {
            found := true
            break
        }
    }
    
    DllCall("GlobalFree", "UPtr", p)
    
    ; Переключаем раскладку
    if (found) {
        DllCall("ActivateKeyboardLayout", HKL, "UInt", layoutID, "UInt", 0)
    }
}

2. Использование ControlSend и SendInput

Другой подход - использовать комбинацию ControlSend и SendInput для имитации системной комбинации переключения раскладки.

autohotkey
~LCtrl up::
    If (A_PriorKey = "LControl") {
        ControlSend,, {Ctrl down}{Shift down}{Ctrl up}{Shift up}, A
    }
return

~RCtrl up::
    If (A_PriorKey = "RControl") {
        ControlSend,, {Ctrl down}{Shift down}{Ctrl up}{Shift up}, A
    }
return

Однако этот метод не всегда надежен, так как как отмечает AutoHotkey Documentation, стандартные методы отправки клавиш могут не работать в системных диалогах.


Решение через Windows API и IMM

Наиболее надежным решением является использование прямых вызовов Windows API. Вот улучшенный вариант:

autohotkey
~LCtrl up::
    If (A_PriorKey = "LControl") {
        SetKeyboardLayout(0x0409) ; Английский (США)
    }
return

~RCtrl up::
    If (A_PriorKey = "RControl") {
        SetKeyboardLayout(0x0419) ; Русский
    }
return

SetKeyboardLayout(layoutID) {
    static WM_INPUTLANGCHANGEREQUEST := 0x50
    
    ; Получаем активное окно
    hwnd := WinExist("A")
    
    ; Если это системный диалог, используем альтернативный метод
    if (IsSystemDialog(hwnd)) {
        UseIMMMethod(layoutID)
    } else {
        ; Стандартный SendMessage для обычных окон
        SendMessage, WM_INPUTLANGCHANGEREQUEST, 0, layoutID,, ahk_id %hwnd%
    }
}

IsSystemDialog(hwnd) {
    ; Проверяем, является ли окно системным диалогом
    WinGetTitle, title, ahk_id %hwnd%
    WinGetClass, class, ahk_id %hwnd%
    
    ; Системные диалоги часто имеют эти классы
    if (class = "#32770") or (class = "#32768") or (InStr(title, "Выберите файл") or InStr(title, "Открыть") or InStr(title, "Сохранить"))
        return true
    
    return false
}

UseIMMMethod(layoutID) {
    ; Используем IMM32.dll для переключения раскладки
    static hIMM := 0
    if (!hIMM)
        hIMM := DllCall("LoadLibrary", "Str", "imm32.dll", "UPtr")
    
    ; Получаем дескриптор окна
    hwnd := WinExist("A")
    
    ; Получаем стандартное окно IME
    imeWnd := DllCall("imm32\ImmGetDefaultIMEWnd", "UPtr", hwnd, "UPtr")
    
    ; Отправляем сообщение о смене языка
    SendMessage, 0x50, 0, layoutID,, ahk_id %imeWnd%
}

Этот метод комбинирует SendMessage для обычных окон и альтернативный подход через IMM для системных диалогов.


Решение через ControlSend и SendInput

Для системных диалогов можно использовать ControlSend с указанием конкретного контрола:

autohotkey
~LCtrl up::
    If (A_PriorKey = "LControl") {
        SendToDialog("English")
    }
return

~RCtrl up::
    If (A_PriorKey = "RControl") {
        SendToDialog("Russian")
    }
return

SendToDialog(layout) {
    ; Определяем, какой метод использовать в зависимости от типа окна
    WinGetTitle, title, A
    
    if (InStr(title, "Выберите файл") or InStr(title, "Открыть") or InStr(title, "Сохранить")) {
        ; Для диалогов сохранения/открытия файлов
        ControlFocus, Edit1, A
        ControlSend, Edit1, {Ctrl down}{Shift down}{Ctrl up}{Shift up}, A
    } else {
        ; Для других системных окон
        ControlSend,, {Ctrl down}{Shift down}{Ctrl up}{Shift up}, A
    }
}

Решение через системный диалог языков

Еще один подход - использование системного диалога управления языками:

autohotkey
~LCtrl up::
    If (A_PriorKey = "LControl") {
        ; Открываем диалог языков
        Run, rundll32.exe Shell32.dll,Control_RunDLL input.dll,,{C07337D3-DB2C-4D0B-9A93-B722A6C106E2}
        
        ; Ждем открытия диалога
        if WinWaitActive("Text Services and Input Languages", "", 3) {
            ; Переключаем на нужную раскладку
            Send, ^{Tab}!c
            
            ; Ждем диалога изменения комбинаций
            if WinWaitActive("Change Key Sequence", "", 3) {
                WinGetPos(, , , , y)
                WinMove, , y + 250
            }
        }
    }
return

Этот метод открывает системный диалог управления языками, но он может быть слишком медленным для повседневного использования.


Полные примеры рабочих скриптов

Вариант 1: Комбинированный подход с проверкой типа окна

autohotkey
#NoEnv
#SingleInstance force

~LCtrl up::
    If (A_PriorKey = "LControl") {
        SwitchToLayout(0x0409) ; Английский (США)
    }
return

~RCtrl up::
    If (A_PriorKey = "RControl") {
        SwitchToLayout(0x0419) ; Русский
    }
return

SwitchToLayout(layoutID) {
    static WM_INPUTLANGCHANGEREQUEST := 0x50
    
    hwnd := WinExist("A")
    
    ; Проверяем, является ли окно системным диалогом
    if (IsSystemDialog(hwnd)) {
        ; Для системных диалогов используем IMM метод
        UseIMMMethod(layoutID)
    } else {
        ; Для обычных окон используем SendMessage
        SendMessage, WM_INPUTLANGCHANGEREQUEST, 0, layoutID,, ahk_id %hwnd%
    }
}

IsSystemDialog(hwnd) {
    WinGetTitle, title, ahk_id %hwnd%
    WinGetClass, class, ahk_id %hwnd%
    
    ; Системные диалоги
    if (class = "#32770") or (class = "#32768")
        return true
    
    ; Диалоги сохранения/открытия файлов
    if (InStr(title, "Выберите файл") or InStr(title, "Открыть") or InStr(title, "Сохранить"))
        return true
    
    return false
}

UseIMMMethod(layoutID) {
    static hIMM := 0
    if (!hIMM)
        hIMM := DllCall("LoadLibrary", "Str", "imm32.dll", "UPtr")
    
    hwnd := WinExist("A")
    imeWnd := DllCall("imm32\ImmGetDefaultIMEWnd", "UPtr", hwnd, "UPtr")
    
    SendMessage, 0x50, 0, layoutID,, ahk_id %imeWnd%
}

Вариант 2: Упрощенный метод с ControlSend

autohotkey
~LCtrl up::
    If (A_PriorKey = "LControl") {
        SendCtrlShiftToActiveWindow()
    }
return

~RCtrl up::
    If (A_PriorKey = "RControl") {
        SendCtrlShiftToActiveWindow()
    }
return

SendCtrlShiftToActiveWindow() {
    ; Получаем активный контрол
    ControlGetFocus, control, A
    
    ; Определяем тип окна
    WinGetTitle, title, A
    WinGetClass, class, A
    
    ; Для системных диалогов
    if (class = "#32770") or (InStr(title, "Выберите файл") or InStr(title, "Открыть") or InStr(title, "Сохранить")) {
        ; Фокусируемся на редакторе
        ControlFocus, Edit1, A
        ControlSend, Edit1, {Ctrl down}{Shift down}{Ctrl up}{Shift up}, A
    } else {
        ; Для обычных окон
        ControlSend,, {Ctrl down}{Shift down}{Ctrl up}{Shift up}, A
    }
}

Рекомендации по выбору метода

Наиболее надежный подход

Комбинированный метод с проверкой типа окна (Вариант 1) является наиболее надежным, так как:

  1. Оптимизирован для разных типов окон: Использует SendMessage для обычных окон и IMM метод для системных диалогов
  2. Минимальная задержка: Не открывает дополнительные диалоги, как некоторые другие методы
  3. Совместимость: Работает на всех версиях Windows 10
  4. Надежность: Использует прямые вызовы Windows API через IMM32

Почему этот метод лучше других:

  • SendMessage в обычных окнах: Работает быстро и эффективно
  • IMM метод в системных диалогах: Обходит ограничения системных окон
  • Проверка типа окна: Автоматически выбирает нужный метод
  • Нет дополнительных настроек: Не требует изменения системных параметров

Дополнительные рекомендации:

  1. Тестируйте скрипт: Проверьте работу в разных системных диалогах (сохранение, открытие, поиск)
  2. Обновляйте AutoHotkey: Используйте последнюю версию AHK (v2.0+)
  3. Добавьте визуальную обратную связь: Можно добавить звуковой сигнал или системное уведомление при переключении
  4. Управляйте языками: Убедитесь, что нужные языки установлены в Windows

Этот подход решает вашу проблему, оставая в рамках требования использовать только Ctrl для переключения раскладки без добавления дополнительных комбинаций клавиш в системных настройках.


Источники

  1. AutoHotkey Community - Switching global keyboard layout
  2. AutoHotkey Community - The keyboard layout set by the SendMessage 0x50 message on Explorer is not persistent
  3. Super User - How do I send a keyboard layout switch event with autohotkey in Windows 7?
  4. AutoHotkey Documentation - Send()
  5. AutoHotkey Community - Replicating the behavior of hotkeys used to switch keyboard layout
  6. AutoHotkey Community - Get/Set input/keyboard layout in Console/UWP/Steam/Dialog windows
  7. Super User - Autohotkey not sending some of the commands

Заключение

Основная проблема вашего скрипта заключается в том, что SendMessage с кодом 0x50 не работает в системных диалогах Windows из-за особенностей их обработки сообщений.

Ключевые решения:

  1. Комбинированный подход: Используйте SendMessage для обычных окон и IMM метод для системных диалогов
  2. Проверка типа окна: Автоматически определять, является ли окно системным диалогом
  3. Прямые вызовы Windows API: Используйте DllCall для вызова функций imm32.dll

Рекомендуемый скрипт использует комбинированный метод с проверкой типа окна, что обеспечивает надежную работу как в обычных окнах, так и в системных диалогах без необходимости добавления дополнительных комбинаций клавиш в системных настройках.

Этот подход полностью соответствует вашим требованиям: оставляет в системе единственный способ переключения языков через Ctrl и не требует установки дополнительных программ вроде PuntoSwitcher.