Windows11  IME日本語/英語でマウスポインタ切替え

いつも半角英語モードだったり日本語モードだったり間違えて時間ロスになるので、今回作ってみました。 AutoHotkey v2を使います。 (【Update】非アクティブウィンドウにも対応しました。【Update】handに対応しました。【Update】*Microsoft Teamsでも動作するようになりました。)

AutoHotkey v2をインストール

  1. 公式ページAutoHotkeyでDownload v2.0をダウンロードする。
  2. ダウンロードされたファイル(私の場合はAutoHotkey_2.0.18_setup.exeでした)をダブルクリックで実行します。

AutoHotkey v2開始

  • 『New script』~『Empty』で『.ahkファイル』を作成します。
  • ファイルは”C:\Users\<user_name>\Documents\AutoHotkey\フォルダに作成されます。

  • 以下のコードを張り付けてファイル名を保存します。 私は、”C:\Users\<user_name>\Documents\AutoHotkey\IMEstatus.ahk”に入れています。
  • 先頭が『;』のところはコメントです。
  • IME_GET()はAutohotkey v2.0のIME制御用 関数群 IMEv2.ahk #AutoHotkey – Qiitaのところのを参考にTeams対応するように変更して使わせてもらっています。
  • 以下のコードを張り付けてファイル名を保存します。 私は、”C:\Users\<user_name>\Documents\AutoHotkey\IMEstatus.ahk”に入れています。
  • 先頭が『;』のところはコメントです。
  • AutoHotkeyのスクリプトは独自スクリプトです。
  • 起動方法は、AutoHotkey v2をインストールしていれば、上記フォルダに以下コードを入れて、その.ahkファイルをダブルクリックで起動できます。
  • マウスポインター*eng*.curのファイルは既存Windowsには無いので、このコードの下のファイルをダウンロードしてC:\Windows\Cursors\に格納してください。
  • 終了方法
    • Shift+Ctrl+Alt+Z
    • タスクバーの『^』クリックして緑色の背景色に白文字『H』のアイコンを右クリックして『Exit』
  • 起動すると.ahkファイルと同じフォルダにMyLog.txtファイルが作成され、『STARTed』とログされます。
  • 終了するとMyLog.txtファイルに『Exit』と表示されます。
  • .curファイルが無いと、1分に1回MyLog.txtファイル追記されます。 ファイルが大きくなって困る場合は、MyFuncLoadCursorFromFile内のエラー追記のヵ所をコメントアウトしてください。
#Requires AutoHotkey v2.0

MyFuncLog("***STARTed****")

global CURSORS := [32512, 32513, 32649]
global i := 0
global hCursorMap:=Map()
hCursorMap["arrow"]       := MyFuncLoadCursorFromFile("C:\Windows\Cursors\aero_arrow.cur")
hCursorMap["arrow_eng"]   := MyFuncLoadCursorFromFile("C:\Windows\Cursors\aero_arrow_eng.cur")
hCursorMap["beam_i"]      := MyFuncLoadCursorFromFile("C:\Windows\Cursors\beam_i.cur")
hCursorMap["beam_i_eng"]  := MyFuncLoadCursorFromFile("C:\Windows\Cursors\beam_i_eng.cur")
hCursorMap["hand"]      := MyFuncLoadCursorFromFile("C:\Windows\Cursors\aero_link.cur")
hCursorMap["hand_eng"]  := MyFuncLoadCursorFromFile("C:\Windows\Cursors\aero_link_eng.cur")

^!+z:: ; Ctrl + Alt + Shift + z で終了
{
    ExitApp
}
OnExit(MyFuncExit)
MyFuncExit(ExitReason, ExitCode) {
    MyFuncLog("OnExit: Exit Reason: " . ExitReason . ", Exit Code: " . ExitCode)
}

SetTimer(CheckIME, 100)
return

CheckIME() {
    global CURSORS
    global hCursorMap
    global i
    i:=i+1
    cursorFile:=""
    IMEStatus := IME_GET()
    for CursorID in CURSORS {
        if (CursorID == 32512){
            if (IMEStatus = 1) {
                MyFuncSetSystemCursor(hCursorMap["arrow"], cursorID)
            } else if (IMEStatus = 0) {
                MyFuncSetSystemCursor(hCursorMap["arrow_eng"], cursorID)
            }
        }
        if (CursorID == 32513){
            if (IMEStatus = 1) {
                MyFuncSetSystemCursor(hCursorMap["beam_i"], cursorID)
            } else if (IMEStatus = 0) {
                MyFuncSetSystemCursor(hCursorMap["beam_i_eng"], cursorID)
            }
        }
        if (CursorID == 32649){
            if (IMEStatus = 1) {
                MyFuncSetSystemCursor(hCursorMap["hand"], cursorID)
            } else if (IMEStatus = 0) {
                MyFuncSetSystemCursor(hCursorMap["hand_eng"], cursorID)
            }
        }
    }
}

IME_GET(WinTitle := "A") {
    ; GUIスレッド情報を取得
    ptrSize := A_PtrSize
    cbSize := 4 + 4 + (ptrSize * 6) + 16
    stGTI := Buffer(cbSize, 0)
    NumPut("UInt", cbSize, stGTI, 0) ; DWORD cbSize;
    if DllCall("GetGUIThreadInfo", "UInt", 0, "Ptr", stGTI) {
        hwnd := NumGet(stGTI, 8 + ptrSize, "Ptr")
    }

    ; IMEの状態を取得
    imeWnd := DllCall("imm32\ImmGetDefaultIMEWnd", "Ptr", hwnd, "Ptr")
    if !imeWnd {
        MsgBox "IME window not found."
        return
    }
    IME_bit := MyFuncGetIME(imeWnd)

    return IME_bit
}

MyFuncGetIME(imeWnd){
    IME_bit := DllCall("SendMessage"
    , "Ptr", imeWnd
    , "UInt", 0x0283  ; Message: WM_IME_CONTROL
    , "Ptr", 0x0005  ; wParam: IMC_GETOPENSTATUS
    , "Ptr", 0)      ; lParam: 0
    return IME_bit
}

MyFuncLoadCursorFromFile(file){
    hCursor:=DllCall("LoadCursorFromFile", "Str", file, "Ptr")
    cursorInfo := Buffer(40)  ; ICONINFO structure size 40bytes
    if (!DllCall("GetIconInfo", "ptr", hCursor, "ptr", cursorInfo) && Mod(i,600) ==0) ) {
        MyFuncLog("Failed LoadCursorFromFile: ERROR code: " . A_LastError . "  cursorFile: " . file . "  CursorHandle: " . hCursor)
    }
    return hCursor
}

MyFuncSetSystemCursor(hCursor, cursorID){
    hCursor := DllCall("CopyImage", "Ptr", hCursor, "UInt", 2, "Int", 0, "Int", 0, "UInt", 0, "Ptr")
    result := DllCall("SetSystemCursor", "Ptr", hCursor, "Int", CursorID)
    if !(result)
        MyFuncLog("SetSystemCursor: CursorID: " . CursorID . " ERROR: " .  A_LastError . " hCursor: " . hCursor )        
    return result
}

MyFuncLog(msg, filePath:="./MyLog.txt"){
    currentTime := FormatTime(A_Now, "yyyy-MM-dd HH:mm:ss")
    LogFile := FileOpen(filePath, "a")
    if LogFile {
        LogFile.WriteLine(currentTime . " : " . msg)
        LogFile.Close()
    } else {
        MsgBox("Failed to open file.:" . filePath)
    }
}

マウスポインタ

  • 英語の時、以下のポインターが表示されるように、既存のaero_arrow.curをカーソルエディタに読み込んで編集しました。
  • “C:\Windows\Cursors\aero_arrow_eng.cur”に保存しています。
  • aero_arrow_eng.cur <—-ダウンロード
  • 上記コード例では以下3つのファイルが無いとエラーとなりポインターが変わりません。
  • 作成するのが面倒臭い場合は、既存の別の.curファイルを作成してコードに記述すればそのイメージに変わると思いますが、.curのファイルサイズの違いがある場合などは検証していないので、同じaero_arrow.cur等と同じサイズで作成するのが良いかもしれません。
  • 半角文字入力の時はこんな感じ。  ”C:\Windows\Cursors\beam_i_eng.cur”に保存してます。
  • beam_i_eng.cur <—-ダウンロード
  • Webのリンク上のハンドにも対応しました。
  • aero_link_eng.cur <—-ダウンロード

*クロスやサイズ等もあるのですが、それらは対応していません。

私の場合は大体ダークモード画面なので、この緑色は非常に見やすいです。 しかし画面が白基調だと、I beamは赤等の方が分かりやすいかもしれませんね。

スタートUp

Windows起動時にこのプログラムを有効にするには

  1. Win+Rを押します
  2. 『shell:startup』でスタートアップフォルダが開きます。
  3. 実行したい.ahkファイルか.ahkをコンパイルした.exeファイルのショートカットをこのスタートアップフォルダに張り付けます。
    • ショートカット作成方法
      • .ahkファイルを左クリックで選択
      • その.ahk上で右クリック
      • 『その他のオプションを確認』
      • 『送る』
      • 『デスクトップにショートカットを作成』
  4. これで次回Windowsを起動した時に自動的に起動します。

非アクティブウィンドウでも動作

上記のコードでは、

  • IME切り替えでWindowsの下のタスクバーに表示されている『A』/『あ』は、現在選択されている(クリックした)アクティブウィンドウ(=アプリ)のIMEの状態が表示されています。
  • 上記のコードではアクティブウィンドウのIMEの状態のポインターの表示となっています。
  • しかしIMEの日本語、半角英語設定は各アプリで異なる為、異なるウィンドウをクリックしてアクティブにすると、そのウィンドウのIME状態の表示となり、ポインターの見た目も更新されます。
  • しかし、私は非アクティブウィンドウ上にポインターが移動(Hover)したら、ポインター直下のウィンドウが非アクティブでもアクティブでも、ポインタ直下のウィンドウのIME状態でポインタの表示を変えたかったので、その方法を以下に記述します。(理由は、入力するアプリをクリックするまで日本語か半角英語か分からなければ、若干の遅れが出るので、入力フィールドに到達する前に状態を知って入力したい状態に切り替えたいからです。)
  • IME_GET()の部分だけ以下に入れ替えます。
  • 違いは、MouseGetPosでポインタ直下の非アクティブでもhwndを取得してポインターの見た目を変えています。
  • しかし『Microsoft Teams』だけは非アクティブ時に正しいhwndが取得できずポインターの見た目を変える事が出来ていないので、GetGUIThreadInfoでアクティブの場合のみ正しいhwndを取得してポインタの見た目を変えるようにしています。 TeamsのIMEの状態は、Teamsのウィンドウをクリックしてアクティブにすれば正しく表示されます。
  • 以下のコードでは非アクティブのウィンドウであってもTeams以外ではポインタの見た目は変わりますが、Windowsのタスクバーの『A』/『あ』表示は、アクティブのウィンドウに紐づいているので、ポインタの見た目は変わっても、『A』/『あ』表示は変わらないので、ポインタの見た目と『A』/『あ』が異なる場合があります。 
  • しかし、ポインタ直下のウィンドウをクリックしてアクティブにすれば、『A』/『あ』表示もポインタの見た目と同じになります。
  • Teamsはアクティブ時だけ正しく動作と言う点と、上記の『A』/『あ』との表示違いの2点がありますが、上記コードでもポインター移動中もポインタはアクティブウィンドウのIME状態しか示していないので直下のウィンドウのIME状態と異なり、何か異なる事に関してはどちらもどちらです。 私は大画面で作業しているので、いちいち目線をタスクバーに落としたくないので、効率アップの為現在見ているポインタの状態で判断したいので下のコードが気に入っています。
IME_GET() {
    ; マウスカーソルの位置にあるウィンドウのハンドルを取得
    MouseGetPos(&x, &y, &hwnd)
    hWnd2:=hWnd
    if !hwnd {
        MsgBox "No window under mouse cursor."
        return
    }
    
    ;IMEの状態を取得
    imeWnd := DllCall("imm32\ImmGetDefaultIMEWnd", "Ptr", hwnd, "Ptr")
    imeWnd2:= imeWnd
    if InStr(WinGetTitle(hwnd),"Microsoft Teams"){
        IME_bit:=3
    } else {
         if imeWnd{
            IME_bit := MyFuncGetIME(imeWnd)
            return IME_bit
         } else {
            IME_bit:=4
         }
    }
    IME_bit2:=IME_bit

    ; GUIスレッド情報を取得---これがあるとTeamsは動作するが非アクティブのWindow上ではIME情報取得できない。
    ptrSize := A_PtrSize
    cbSize := 4 + 4 + (ptrSize * 6) + 16
    stGTI := Buffer(cbSize, 0)
    NumPut("UInt", cbSize, stGTI, 0) ; DWORD cbSize;
    if DllCall("GetGUIThreadInfo", "UInt", 0, "Ptr", stGTI) {
        hwnd := NumGet(stGTI, 8 + ptrSize, "Ptr")
    }
    
    ; IMEの状態を再取得
    imeWnd := DllCall("imm32\ImmGetDefaultIMEWnd", "Ptr", hwnd, "Ptr")
    if imeWnd {
        IME_bit := MyFuncGetIME(imeWnd)
    }
    
    ; MyFuncLog("hwnd: " . hwnd2 . " / " . hwnd . ",  imeWnd: " . imeWnd2 . " / " . imeWnd `
    ;             . ",  IME_bit: " . IME_bit2 . " / " . IME_bit)
    return IME_bit
}
  • hwnd2、imeWnd2、IME_bit2はデバッグ用で残しています。
  • これらによってTeamsの挙動が他のアプリと異なる事が分かりました。
  • IMEstatus.ahk <—-非アクティブウィンドウでもポインタの見た目が変わるフルコードのダウンロード

日本語、半角英語切り替えできる場所

  1. デスクトップ上入力モード切替可能
  2. ChromeやEdge等ブラウザ
    • 文字入力できるフィールド選択されている場合入力モード切替可能
    • ブラウザの文字入力以外の部分では入力モード切替不可
  3. TeamsもElectronで作られているのでChromeやEdgeと同じです。
  4. VScodeもChromiumベースなのでChromeやEdgeと同じです。
  5. PowerPoint
    • シートが表示されている状態では何処でも入力モード切替可能
  6. Outlook
    • メッセージ作成ウィンドは何処でも入力モード切替可能
    • 受信トレイ等
      • リストをクリックすると入力モード切替可能
      • 受信や送信済みの内容上では入力モード切替不可
  7. Excel
    • シートが表示されている場合
      • シート上はポインタ形状がプラスなのでポインタの見た目は変わりませんが入力モード切替可能
      • メニューやタイトルバー等では見た目も変わり入力モード切替可能
  8. Word
    • シートが表示されている場合は何処でも入力モード切替可能

苦労した点

  • 最初、他のサンプルから毎回(0.1秒ごと)FileからLoadCursorFromFileでhCursorに変換してSetSystemCursorで指定していました。 しかし何故か毎回起動5~6分後に動作止まってしまいました。 
  • そこでhCursor作成は最初に1度だけにしてhCorsorMapに入れる事にしました。 このhCursorMapはずっと有効である事を確認しました。
  • これで止まらなくなったのですが何故かこれではマウスポインタの見た目は1度変更したら、2度と変更できませんでした。 
  • そこでhCurosorMapからCopyImageでhCursorを作り直してSetSystemCursorすると何度でもトグル出来て止まらなくなりました。
  • しかし何故かTeamsだけ言う事を聞かない。気持ち悪い。 しかし今は時間がないのでここまで。 だれかTeamsでも動作できるようになったら教えて貰えると嬉しいです?
  • VScodeで記述していましたが、ExtensionをInstallしたら格段に編集しやすくなりました。 私がInstallしたのはMark WiemerさんのAutoHotkey Plus Plusです。 しかしこれは紹介だけでAutoHotkey Plus Plusの使用は自己責任でお願いします。
  • Teamsは他のアプリと挙動が違うので何が原因か分かりませんでした。 今でもTeamsの非アクティブの時に正しいhwndが取得できずポインターの見た目を変える事が出来ていません。 そのうち何とかしたいなぁ。

コメント