gists

; ==========================================
; AHK v1.1: Active WINDOW changed -> IME OFF
; - ignores tab changes (same foreground window)
; - robust: uses SetWinEventHook(EVENT_SYSTEM_FOREGROUND)
; - targets focused control for IME (works better for Chrome/VSCode etc.)
; ==========================================

#NoEnv
#SingleInstance Force
#Persistent
SetBatchLines, -1

global g_LastTop := 0
global g_PendingTop := 0
global g_hWinEventHook := 0
global g_pWinEventProc := 0

InitWinEventHook()
return


InitWinEventHook()
{
    global g_hWinEventHook, g_pWinEventProc

    EVENT_SYSTEM_FOREGROUND := 0x0003
    WINEVENT_OUTOFCONTEXT   := 0x0000
    WINEVENT_SKIPOWNPROCESS := 0x0002

    ; コールバックを保持(変数をグローバルで保持するのが重要)
    g_pWinEventProc := RegisterCallback("WinEventProc", "Fast", 7)

    g_hWinEventHook := DllCall("user32\SetWinEventHook"
        , "UInt", EVENT_SYSTEM_FOREGROUND
        , "UInt", EVENT_SYSTEM_FOREGROUND
        , "Ptr", 0
        , "Ptr", g_pWinEventProc
        , "UInt", 0
        , "UInt", 0
        , "UInt", WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS
        , "Ptr")

    if (!g_hWinEventHook)
    {
        MsgBox, 16, Error, SetWinEventHook failed.
        ExitApp
    }

    ; SetWinEventHook を受けるにはメッセージループが必要(#Persistent で満たす) :contentReference[oaicite:2]{index=2}
    OnExit("CleanupWinEventHook")
}

CleanupWinEventHook(reason := "", code := 0)
{
    global g_hWinEventHook
    if (g_hWinEventHook)
    {
        DllCall("user32\UnhookWinEvent", "Ptr", g_hWinEventHook)
        g_hWinEventHook := 0
    }
}

WinEventProc(hWinEventHook, event, hwnd, idObject, idChild, idEventThread, dwmsEventTime)
{
    global g_LastTop, g_PendingTop

    if (!hwnd)
        return

    ; 念のため「トップレベル(ルート)ウィンドウ」に正規化
    hwndTop := DllCall("user32\GetAncestor", "Ptr", hwnd, "UInt", 2, "Ptr") ; GA_ROOT=2
    if (!hwndTop)
        hwndTop := hwnd

    ; 同一トップレベルなら何もしない(タブ切替などはここで止まる)
    if (hwndTop = g_LastTop)
        return

    g_LastTop := hwndTop
    g_PendingTop := hwndTop

    ; コールバック内で重い処理をしない(遅延実行)
    SetTimer, __DoImeOff, -30
}

__DoImeOff:
    global g_PendingTop
    hwndTop := g_PendingTop
    if (!hwndTop)
        return

    ; 実際に入力している「フォーカス先 hwnd」を狙う
    hwndFocus := GetForegroundFocusHwnd()
    if (hwndFocus)
        IME_SetOpenStatus(hwndFocus, 0)

    ; 念のためトップレベルにも
    IME_SetOpenStatus(hwndTop, 0)
return


GetForegroundFocusHwnd()
{
    ; GUITHREADINFO 構造体で foreground thread の hwndFocus を取る
    cbSize := 8 + (A_PtrSize * 6) + 16
    VarSetCapacity(gti, cbSize, 0)
    NumPut(cbSize, gti, 0, "UInt")

    if !DllCall("user32\GetGUIThreadInfo", "UInt", 0, "Ptr", &gti)
        return 0

    ; offset: cbSize(4) flags(4) hwndActive(Ptr) hwndFocus(Ptr)
    return NumGet(gti, 8 + A_PtrSize, "Ptr")
}

IME_SetOpenStatus(hwnd, state)
{
    ; state: 0=OFF, 1=ON

    ; 1) IMM32 の API で閉じる(効くアプリが多い)
    hIMC := DllCall("imm32\ImmGetContext", "Ptr", hwnd, "Ptr")
    if (hIMC)
    {
        DllCall("imm32\ImmSetOpenStatus", "Ptr", hIMC, "Int", state)
        DllCall("imm32\ImmReleaseContext", "Ptr", hwnd, "Ptr", hIMC)
    }

    ; 2) 既定IMEウィンドウへ WM_IME_CONTROL でも試す(保険)
    hIME := DllCall("imm32\ImmGetDefaultIMEWnd", "Ptr", hwnd, "Ptr")
    if (hIME)
        SendMessageTimeoutW(hIME, 0x0283, 0x0006, state, 80) ; WM_IME_CONTROL / IMC_SETOPENSTATUS
}

SendMessageTimeoutW(hwnd, msg, wParam, lParam, timeout := 100)
{
    ; ハング回避(SMTO_ABORTIFHUNG)
    ok := DllCall("user32\SendMessageTimeoutW"
        , "Ptr", hwnd
        , "UInt", msg
        , "Ptr", wParam
        , "Ptr", lParam
        , "UInt", 0x0002
        , "UInt", timeout
        , "PtrP", out
        , "Ptr")
    return ok ? out : ""
}

これは何してます?

これは 「アクティブ(最前面)ウィンドウが切り替わった瞬間に、そのウィンドウの IME を強制的に OFF にする」 AHK v1.1 スクリプトです。タブ切り替えのように「同じアプリの同じトップレベルウィンドウ内の変化」は無視するように作られています。


全体の動き(何が起きるか)

  1. Windows のイベントフック SetWinEventHook(EVENT_SYSTEM_FOREGROUND) を使って、フォアグラウンド(最前面)ウィンドウが変わったことを監視します。
  2. 変化があったら、そのウィンドウの
    • 実際に入力しているフォーカス先コントロール(hwndFocus)
    • 念のため トップレベルウィンドウ(hwndTop)

    の両方に対して IME を OFF(閉じる) する処理を行います。

結果として、別のアプリに切り替えるたびに「IME が勝手に ON のままになる」状況を抑制します。


「タブ切替を無視する」仕組み

WinEventProc 内で、渡された hwnd を GetAncestor(..., GA_ROOT)トップレベル(ルート)ウィンドウに正規化しています。

そして、

if (hwndTop = g_LastTop)
    return

となっているので、例えば Chrome のタブ切り替えのように 同じトップレベルウィンドウのままなら、イベントが来ても何もしません。


なぜ SetTimer で遅延している?

WinEventHook のコールバック WinEventProc の中で重い処理をすると不安定になりやすいので、

SetTimer, __DoImeOff, -30

30ms 後に別処理として IME OFF を実行しています(コールバックを軽く保つため)。


IME を OFF にする方法(2段構え)

IME_SetOpenStatus(hwnd, 0) が「IME OFF」を担当します。

中では2つの方法を試しています:

1) IMM32 API(標準的)

これが効くアプリが多い。

2) 保険として WM_IME_CONTROL

さらに SendMessageTimeoutW を使っていて、相手がハングしてても固まりにくい(SMTO_ABORTIFHUNG)ようにしています。


「フォーカス先 hwnd」を取っている理由

GetForegroundFocusHwnd()GetGUIThreadInfo を使って 現在入力対象のコントロール (hwndFocus) を取ります。

Chrome/VSCode みたいに「トップレベルに対して IME をいじっても効きにくい」アプリがあるので、実際の入力欄(フォーカス先)を狙うほうが成功率が上がります。


まとめ(このスクリプトがしていること)


用途としては「ウィンドウを切り替えるたびに IME を必ず英数(OFF)に戻したい」系の常駐ツールですね。必要なら「特定アプリだけ除外する」「特定アプリは ON にする」みたいな分岐も追加できます。