AutoHotKey V2 による HHKB Studio のカスタマイズ

# 概要

AutoHotKey v2 を使って HHKB Studio (日本語配列) をカスタマイズした内容の備忘録です。
変換・無変換キー、マウスボタンへファンクションキーを割り当てて、それらに対してホットキーを割り当てています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8721/cebbeb51-0508-8a8d-000d-a44ffadb60a1.png)


 # AutoHotKey のインストールとスクリプトの実行方法
 
 公式サイトから "Latest Installer" を落としてインストールします。
 
 https://www.autohotkey.com/v2/

 この記事作成時点での作業環境は以下の通りです。
  - Windows 11 23H2
  - HHKB Studio B2.07 (日本語配列キーボードをUS配列の認識環境で利用)
  - AutoHotKey v2.0.10

V2は公開されてから日が浅いため、V1よりも情報が少ないですが、公式リファレンスを参考にコンフィグを作っていきます。

https://ahkscript.github.io/ja/docs/v2/index.htm

適当なテキストエディタを使って「script.ahk」というファイルを作成しました。
拡張子が関連付けされているので、スクリプトをそのままダブルクリックすると、AHKが起動してスクリプトを読み込んでくれるのですが、タスクマネージャなどの管理者権限で起動しているアプリに対しては、AHK自身も管理者権限で起動しないと入力を受け付けてくれません。このためスクリプトへのショートカットファイルを作成して、ファイルプロパティから管理者権限で起動する設定を行いました。

<table>
  <tr>
    <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8721/29b389c8-4d0c-8170-68bd-6e29a6318f86.png"></td>
    <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8721/49e5614f-3850-791b-ac98-4c7534d9f104.png"></td>
  </tr>
</table>

# 機能の有効化、AHKの操作

まずはAutoHotkey自体の設定と操作するホットキーを定義します。

```ahk:機能の有効化、AHKの操作
#Requires AutoHotkey v2.0

#SingleInstance Force
#UseHook
InstallKeybdHook                ; キーボードフックの有効化
ProcessSetPriority "Realtime"   ; プロセスの優先順位

; AHKの操作
#SuspendExempt
^!s::Suspend    ; Ctrl+Alt+S - 一時停止
^!r::Reload     ; Ctrl+Alt+R - リロード
^!e::Edit       ; Ctrl+Alt+E - エディット
^!k::KeyHistory ; Ctrl+Alt+K - キーヒストリー
^!l::ListHotKeys ; Ctrl+Alt+L - ホットキーの一覧
^!Esc::ExitApp  ; Ctrl+Alt+Esc - AHK終了
#SuspendExempt False
```

https://ahkscript.github.io/ja/docs/v2/lib/InstallKeybdHook.htm

https://ahkscript.github.io/ja/docs/v2/lib/ProcessSetPriority.htm

https://ahkscript.github.io/ja/docs/v2/lib/_SuspendExempt.htm

# カーソル操作を行うためのホットキー
無変換キーへ配置したF18キーを押している間は、ホームポジションキーに対してカーソル操作系のホットキーを割り当てます。
単体でタップした場合は `AppsKey` が送信されます。

```
; テキスト編集機能
F18::
{
    OutputDebug ThisHotkey
    global SelectMode := 0
    KeyWait("F18")

    if (IsSingleTap("F18")) {
        Send("{AppsKey}")
    }
}
#HotIf GetKeyState("F18", "P")
    q::!Left                        ; Back Page
    w::SelectMode_Send("^{Home}")   ; Head of Page
    e::SelectMode_Send("^{End}")    ; End of Page
    r::!Right                       ; Next Page
    t::return
    y::^y
    u:: global SelectMode += 1      ; Change SelectMode
    i::SelectMode_Send("{Home}")    ; Head of Line
    o::SelectMode_Send("{End}")     ; End of Line
    p::return

    a::SelectMode_Send("{PgUp}")
    s::SelectMode_Send("{Up}")
    d::SelectMode_Send("{Down}")
    f::SelectMode_Send("{PgDn}")
    g::Del
    h::BackSpace
    j::SelectMode_Send("^{Left}")   ; Left Word
    k::SelectMode_Send("{Left}")
    l::SelectMode_Send("{Right}")
    `;::SelectMode_Send("^{Right}") ; Right Word

    z::^z
    x::^x
    c::^c
    v::^v
    b::return
    n::return
    m::Enter
    ,::^BackSpace  ; Delete Left Word
    .::^Del        ; Delete Right Word
    /::return
#HotIf
```

ホットキー単体でタップされたかどうかを判定する関数。

```
; シングルタップの判定
IsSingleTap(thisHotkey, tappingTerm := TAPPING_TERM)
{
    OutputDebug "IsSingleTap"
    OutputDebug " - " thisHotkey
    OutputDebug " - " A_PriorKey
    OutputDebug " - " A_TimeSinceThisHotkey
    if (A_PriorKey != thisHotkey) {
        return false ; 異なるキーが押された
    }
    else if (tappingTerm < 0) {
        return true
    }
    else if (A_TimeSinceThisHotkey > tappingTerm) {
        return false ; 時間のしきい値を超えた
    }
    else {
        return true
    }
}

```

カーソル移動キーに対して選択モードを切り替えるための関数。`SelectMode` が有効な場合は `Shift` による範囲選択または `Shift+Ctrl+Alt` による矩形選択が有効となります。

```
; カーソル範囲選択モード
SelectMode := 0
SelectMode_Send(keys)
{
    mod_key := ""
    if (SelectMode) {
        Switch Mod(SelectMode, 2)
        {
        Case 1:
            mod_key := "+"      ; Shift
        Default:
            mod_key := "+^!"    ; Shift+Ctrl+Alt
        }
    }
    Send mod_key keys
}
```

# ウィンドウ操作機能
変換キーへ配置したF19のホットキーでは、OS機能のウィンドウスナップと、AutoHotkeyによるウィンドウのリサイズ機能を割り当てています。
単体でタップした場合は Win キーを送信します。

```
; ウィンドウ操作機能
F19::
{
    OutputDebug ThisHotkey
    global WinLock := true
    KeyWait("F19")

    global WinLock := false
    if (IsSingleTap("F19")) {
        Send("{RWin}")
    }
}
#HotIf GetKeyState("F19", "P")
    ; Window Snap
    w::Send "#{Up}"
    e::Send "#{Down}"
    i::Send "#{Left}"
    o::Send "#{Right}"

    swqt := A_ScreenWidth/4
    a::WinResize("a",     0, -swqt)
    s::WinResize("s",     0,   -10)
    d::WinResize("d",     0,    10)
    f::WinResize("f",     0,  swqt)
    j::WinResize("j", -swqt,     0)
    k::WinResize("k",   -10,     0)
    l::WinResize("l",    10,     0)
    `;::WinResize(";", swqt,     0)
#HotIf
```

ウィンドウリサイズを行う関数

```
; ウィンドウをリサイズ
WinResize(inputKey, dw, dh)
{
    OutputDebug "WinResize " inputKey " " dw " " dh

    try {
        ; タスクバーの高さ(taskbarHeight)、アクティブなウィンドウタイトル(winTitle)を取得
        WinGetPos(,,, &taskbarHeight, "ahk_class Shell_TrayWnd")
        winTitle := WinGetTitle("A")

        ; ワンショットでリサイズした後、連続入力を受け付ける経過時間まで待機
        WinResize()
        KeyWait(inputKey, "T0.5")

        ; 連続リサイズ開始
        if GetKeyState(inputKey, "P") {
            SetTimer WinResize, 20
        }
        KeyWait(inputKey, "T1")

        ; リサイズ幅を大きくする
        if GetKeyState(inputKey, "P") {
            dw *= 2
            dh *= 2
        }
        KeyWait(inputKey)

        ; 画面縁に内接するようにウィンドウサイズを変更する
        WinResize()
        {
            WinGetPos &x, &y, &width, &height, winTitle
            width  := Min(width + dw, A_ScreenWidth)
            height := Min(height + dh, A_ScreenHeight - taskbarHeight)
            x := Min(x, A_ScreenWidth - width)
            y := Min(y, A_ScreenHeight - height - taskbarHeight)
            WinMove x, y, width, height, winTitle
        }
    }
    catch as e {
        OutputDebug " - catch as e.Message: " e.Message
    }

    SetTimer WinResize, 0
}
```

# モディファイヤ状態をロックするための定義

モディファイヤのロック状態をグローバル変数に保存しておき、これらが `true` 状態のときにはモディファイヤキー付きでキー入力を行うための関数を定義。

```
; モディファイヤ状態を付加してキー送信
ShiftLock := false
CtrlLock := false
WinLock := false
pShiftLock := &ShiftLock
pCtrlLock := &CtrlLock
pWinLock := &WinLock
ModLock_Send(keys)
{
    ; モディファイヤキーのロック
    mod_key := (ShiftLock ? "+" : "") . (CtrlLock ? "^" : "") . (WinLock ? "#" : "")
    Send mod_key keys
}
```

"p"プレフィックスの付いた変数に、変数への参照を格納しておくことで、関数引数への参照渡しをglobal宣言せずに行うことができます。

```
; グローバル変数とその参照
ShiftLock := false
pShiftLock := &ShiftLock

; 宣言無しで参照演算子(&)を使うと、ローカル変数扱いとなるためエラー
a::Function(&ShiftLock)

; global宣言した後なら、グローバル変数への参照演算子(&)を使用可能
b::
{
    global ShiftLock
    Function(&ShiftLock)
}

; 演算子無しでグローバル変数を参照することは可能
c::Function(pShiftLock)

```

https://ahkscript.github.io/ja/docs/v2/Functions.htm#Locals

> 想定ローカル関数内の変数参照は、それが読み込まれるだけであれば、グローバル変数に解決される可能性があります。ただし、変数が代入や参照演算子(&)で使用された場合、デフォルトで自動的にローカルになります。これにより、関数は関数内で宣言することなく、グローバル変数の読み込みや、グローバル関数や組み込み関数の呼び出しを行うことができます。

主な文字入力キー全てに `ModLock_Send` でホットキーを設定します。

```
; モディファイヤロック
#HotIf ShiftLock or CtrlLock or WinLock
    1::
    2::
    3::
    4::
    5::
    6::
    7::
    8::
    9::
    0::ModLock_Send(A_ThisHotkey)

    q::
    w::
    e::
    r::
    t::
    y::
    u::
    i::
    o::
    p::ModLock_Send(A_ThisHotkey)

    a::
    s::
    d::
    f::
    g::
    h::
    j::
    k::
    l::
    `;::ModLock_Send(A_ThisHotkey)

    z::
    x::
    c::
    v::
    b::
    n::
    m::
    ,::
    .::
    /::ModLock_Send(A_ThisHotkey)

    -::
    =::
    \::
    [::
    ]::
    '::
    `::ModLock_Send(A_ThisHotkey)

    Tab::
    sc07D::
    sc02B::
    sc073::ModLock_Send("{" A_ThisHotkey "}")
#HotIf
```

# 左ボタン機能

左ボタンへ配置したF15キーには、ホールド中はShiftキー、タップやカーソル移動時は左クリックとして機能するホットキーを定義します。

```
; 左ボタンの操作
LButtonFunction(thisHotkey, inputKey, modLock)
{
    OutputDebug "LButtonFunction " thisHotkey
    static mouseBtn      := "LButton"
    static mouseBtn_Down := "{Blind}{LButton Down}"
    static mouseBtn_Up   := "{Blind}{LButton Up}"
    static mouseBtn_Tap  := "{Blind}{LButton}"

    try {
        MouseGetPos , , &varWin, &varControl
        OutputDebug " - MouseGetPos " varWin ", " varControl
    }
    catch as e {
        OutputDebug " - catch error message: " e.Message
    }

    ; modLockをホールド
    %modLock% := True

    ; inputKeyホールド中にカーソル移動したらマウスボタンを押下
    while GetKeyState(inputKey, "P") {
        if (A_TimeIdleMouse < 20) {
            Send(mouseBtn_Down)
            break
        }
        Sleep 0
    }
    KeyWait(inputKey)
    %modLock% := False
    
    ; マウスボタンの後処理
    if (GetKeyState(mouseBtn)) {
        ; マウスホールドを開放
        OutputDebug " - HoldUp " inputKey
        Send(mouseBtn_Up)
    }
    else if (IsSingleTap(thisHotkey)) {
        ; マウスボタンのタップ
        OutputDebug " - Tap " inputKey
        Send mouseBtn_Tap
    }
}

; 左ボタン機能
F15::LButtonFunction(ThisHotkey, "F15", pShiftLock)

```

マウス操作関連の機能を使うため、下記定義もスクリプト先頭に追加しておきます。

```
; マウス関連の設定
InstallMouseHook                ; マウスフックの有効化
CoordMode "Mouse", "Screen"     ; マウス座標系の基準
```

左ボタン(F15)＋中ボタン(F16)の同時押しでウィンドウをカーソルで掴むように移動させます。

```
#HotIf GetKeyState("F15", "P")
    ; ウィンドウをカーソル移動に追従
    F16::
    {
        MouseGetPos &mx, &my, &wid
        winTitle := "ahk_id " wid
        WinGetPos &wx, &wy, &width, &height, winTitle
        dx := wx - mx
        dy := wy - my
        while GetKeyState("F16", "P") {
            MouseGetPos &mx, &my
            wx := dx + mx
            wy := dy + my
            WinMove wx, wy, width, height, winTitle
            Sleep 20
        }
    }
#HotIf
```


# 中ボタン機能
中ボタンへ配置したF16キーには、ホールド中はホイール操作、タップしたら中ボタンとして機能するホットキーを定義します。

```
; カーソル移動によるホイール操作
MButtonFunction(thisHotkey, inputKey)
{
    OutputDebug "MButtonFunction " thisHotkey
    static mousePerWheel := 20
    static mouseBtn_Tap  := "{Blind}{MButton}"
    MouseGetPos &downX, &downY
    integralX := 0
    integralY := 0
    wheelX := 0
    wheelY := 0
    cursorMoved := false
    
    ; inputKeyホールド中にカーソル移動したらホイール操作
    while GetKeyState(inputKey, "P") {
        ; マウスの移動量
        MouseGetPos &mx, &my
        DllCall("SetCursorPos", "int", downX, "int", downY)
        deltaX := mx - downX
        deltaY := my - downY

        ; マウス移動量が大きい方向にホイール操作
        if (Abs(deltaX) > Abs(deltaY)) {
            integralX += deltaX
            SendWheel(integralX, &wheelX, "WheelLeft", "WheelRight")
            cursorMoved := true
        }
        else if (Abs(deltaY) > 0) {
            integralY += deltaY
            SendWheel(integralY, &wheelY, "WheelUp", "WheelDown")
            cursorMoved := true
        }
        SendWheel(mousePos, &wheelPos, wheelBackward, wheelForward)
        {
            destination := mousePos // mousePerWheel
            if (destination = wheelPos) {
                ; pass
            }
            else {
                count := Abs(destination - wheelPos)
                which := (destination > wheelPos) ? wheelForward : wheelBackward
                MouseClick which,,, count
                wheelPos := destination

            }
        }

        Sleep 20
    }

    ; カーソル移動量があるなら
    if (cursorMoved) {
        ; pass
    }
    ; タップ入力ならば
    else if (IsSingleTap(thisHotkey)) {
        Send mouseBtn_Tap
    }
}

; 中ボタン
F16::MButtonFunction(ThisHotkey, "F16")

```

中ボタン(F16)ホールド中の`g h b n` キーへホイール操作を割り当てています。

```
#HotIf GetKeyState("F16", "P")
    g::
    h::MouseClick "WheelUP",,, 1
    b::
    n::MouseClick "WheelDown",,, 1
#HotIf
```

# 右ボタン機能

右ボタンへ配置したF17キーには、ホールド中はシフトキーのホールド及び、マウスカーソル移動が低速になるように `SystemParametersInfo` を書き換える機能を割り当てました。
単体でタップした場合は、右ボタンが押下されます。

```
; 右ボタンの操作
RButtonFunction(thisHotkey, inputKey, modLock)
{
    OutputDebug "RButtonFunction " thisHotkey
    static mouseBtn_Down := "{Blind}{RButton Down}"
    static mouseBtn_Up   := "{Blind}{RButton Up}"
    static mouseBtn_Tap  := "{Blind}{RButton}"
    
    ; modLockをホールド
    %modLock% := True

    static SPI_GETMOUSESPEED := 0x70
    static SPI_SETMOUSESPEED := 0x71
    static OrigMouseSpeed := 0
    
    ; マウススピード取得
    DllCall("SystemParametersInfo", "UInt", SPI_GETMOUSESPEED, "UInt", 0, "Ptr*", &OrigMouseSpeed, "UInt", 0)
    ; マウススピードを低速に設定
    DllCall("SystemParametersInfo", "UInt", SPI_SETMOUSESPEED, "UInt", 0, "Ptr", 1, "UInt", 0)

    ; ホールド解除されるまで待機
    KeyWait(inputKey)
    %modLock% := False
    
    ; マウススピードを元に戻す
    DllCall("SystemParametersInfo", "UInt", SPI_SETMOUSESPEED, "UInt", 0, "Ptr", OrigMouseSpeed, "UInt", 0)  ; Restore the original speed.

    ; タップ入力ならば、ボタンタップを送信
    if (IsSingleTap(thisHotkey))
    {
        OutputDebug " - Tap " inputKey
        Send mouseBtn_Tap
    }
}

; 右ボタン
F17::RButtonFunction(ThisHotkey, "F17", pShiftLock)

```

# モディファイヤ付きで左右ボタン

左右ボタンをモディファイヤキーといっしょに押した場合には、通常のマウスボタン操作をモディファイヤ付きで送信するように機能を割り当てます。
`GetModifiers` では押されているモディファイヤキーを判定して、モディファイヤ送信用の修飾記号の文字列を返します。

```
GetModifiers()
{
    modifiers := ""
    if GetKeyState("LWin", "P") || GetKeyState("RWin", "P") {
        modifiers .= "#"
    }
    if GetKeyState("LAlt", "P") || GetKeyState("RAlt", "P") {
        modifiers .= "!"
    }
    if GetKeyState("LCtrl", "P") || GetKeyState("RCtrl", "P") {
        modifiers .= "^"
    }
    if GetKeyState("LShift", "P") || GetKeyState("RShift", "P") {
        modifiers .= "+"
    }
    return modifiers
}

; モディファイヤキー付きマウスボタン
*F15::
{
    OutputDebug ThisHotkey
    modifiers := GetModifiers()
    Send modifiers "{LButton Down}"
    KeyWait("F15")
    Send modifiers "{LButton Up}"
}
*F17::
{
    OutputDebug ThisHotkey
    modifiers := GetModifiers()
    Send modifiers "{RButton Down}"
    KeyWait("F17")
    Send modifiers "{RButton Up}"
}

```