ちょい足しキーボード✕AutoHotKey (AHK)の可能性を考える

# はじめに 
ほぼほぼ初めてですがQiitaにも少しでも情報を残しておきたくて書き始めています。
慣れないことですので大目に見てもらえるとありがたいです。
ここでは特にAHKについて書いていきますので、どこかで見かけましたら優しく相手してもらえると嬉しいです。

--- 
# ちょい足しキーボードという製品はご存知でしょうか？
さて、この手の話題で出てきます「ちょい足しキーボード」というもの、いわゆるキーパッドの一つです。
下図のように4キーで構成されている有線のシンプルなものです。
金額は約3000円で小さいので持ち歩けるというようなのも好印象です。
![MK-04_img3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/045a231c-7926-4377-9955-a0a9ee2148dd.jpeg)

---
# 製品の詳細
こちらについて、製品の詳細は下記URLから確認してくださいませ

https://amzn.to/3Xmk1UY

マニュアルはこちら

https://www.itprotech.jp/macro-keyboard/mk-04/20230728_MK-04_manual.pdf

実際の設定画面がこちら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/4cf5c1b2-7a91-4527-97aa-e1e5ef3e64cc.png)

キー選択画面がこちら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/000cdb03-284c-43a3-a8db-d1fa740f6501.png)

この画像からわかると思いますが、最初からF13からF14が用意されているのは大変ありがたいですね。

ですが、その見た目の通り4キーしかないので、カスタムの幅が限られます。

---

# そこでAutoHotKey(AHK)との連携を検討

## なぜF13以降を使うのか
これの理由は明白です。

既存のキーボードにはF12までしか存在しない、かつ、F13からF24までキー認識はされる
というところです。
Windows限定の話でしたら申し訳ないです。

---

## AHKとはなんなのか
AHK、つまり「AutoHotkey」は、Windows環境で動作する無料のオープンソーススクリプト言語で、特に作業の自動化や効率化に特化しています。2003年にChris Mallettによって開発が始まり、現在もコミュニティによって維持・発展されています。主な用途は、キーボードやマウスの操作をプログラム化すること。例えば、特定のキー入力を別のコマンドに置き換えたり、繰り返し作業を一つのショートカットで実行したりできます。
簡単な例を挙げると、「Ctrl + Q」を押すと「Hello, World!」と自動入力するスクリプトを書くことが可能です。コードはこんな感じです：

```AHK
^q::
Send, Hello, World!
return
```
このシンプルさから初心者でも扱いやすく、テキストエディタでスクリプトを書いて「.ahk」ファイルとして保存し、ダブルクリックで実行できます。また、マウスクリックの記録やウィンドウ操作の自動化、さらにはGUI（グラフィカルユーザーインターフェース）の作成も可能で、上級者向けの複雑なタスクにも対応します。
プログラマーだけでなく、一般ユーザーにも人気があり、例えばゲームのマクロ作成や業務でのデータ入力の効率化など、幅広いシーンで活用されています。公式サイト（autohotkey.com）からダウンロードでき、活発なフォーラムやドキュメントも充実しているので、困ったときのサポートも得やすいです。

HP
https://www.autohotkey.com/

---

# ちょい足しキーボードの設定を改めて見ます
この画像の通り、F13からF16までを振ってあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/4cf5c1b2-7a91-4527-97aa-e1e5ef3e64cc.png)

## 4キーなら動作は4つじゃないの？
ここからがAHKと使う人の頭が試されていくところです。
ショートカットを覚えることに抵抗は無いですか？
ショートカットを何個まで作って覚えてられますか？
という試されるような話になります。

## AHKなら組み合わせの下図だけ設定可能
どういうことか

まずコードから
```
F13 & F14::a
F14 & F13::b
```
こちらのようにですね押すキーの順番を認識します。
F13→F14とF14→F13は違うものとして使うことができるということです。
ここまでで勘の良い方は気づくと思います。
4キーの場合下記の下図だけ組み合わせが可能です。

つまり
```math
4C4=24 通り
```
の組み合わせで設定可能です。
そう、AHKではやろうと思えば4キーの同時押しも認識します。
それを利用しようとしたら24通りの設定が可能なんです。
だいぶ可能性が広がってきましたね。
これに準拠した設定についても一応触れていきます。ただ、今回の本題ではないのと、設定後のものを考えるのが面倒なのでパターンだけ示したいと思います。

```AHK
; F13を押すと「Hello」と入力
F13::
Send, Hello
return

; F14を押すと「Notepad」を起動
F14::
Run, notepad.exe
return

; F15を押すとCtrl+C（コピー）をシミュレート
F15::
Send, ^c
return

; F16を押すとメッセージボックスを表示
F16::
MsgBox, You pressed F16!
return

; F13を押しながらF14を押すと「Combination 1」と入力
$F13 & F14::
Send, Combination 1
return

; F13とF14を押しながらF15を押すと「Triple Combo!」と入力
$F13 & F14::
If GetKeyState("F15", "P") {
    Send, Triple Combo!
}
return

$F13 & F14::
If (GetKeyState("F15", "P") && GetKeyState("F16", "P")) {
    MsgBox, All Four Keys!
}
return

```
という感じになってきます
これをさらに延長していくことで24の組み合わせを実際に使用することが可能です。

# ここから本題｜AHKの動的利用の話
この辺からは実際に稼働できるかも確認しながらやっています。

まず下記コードをファイルの最初に設置

```ahk
; F13を押すと「a」を入力する
F13::
    Send a          ; 「a」を送信
    return          ; 処理の終了

; F14を押すと「b」を入力する
F14::
    Send b          ; 「b」を送信
    return          ; 処理の終了

; F15を押すと「c」を入力する
F15::
    Send c          ; 「c」を送信
    return          ; 処理の終了

; F16を押すと「d」を入力する
F16::
    Send d          ; 「d」を送信
    return          ; 処理の終了

; F13を押しながらF14を押すと「F17」キーをシミュレート
F13 & F14::
    Send {F17}      ; 仮想キー「F17」を送信（F17が使える環境が必要）
    return          ; 処理の終了

; F14を押しながらF13を押すと「F18」キーをシミュレート
F14 & F13::
    Send {F18}      ; 仮想キー「F18」を送信（F18が使える環境が必要）
    return          ; 処理の終了

; F13を押しながらF15を押すと「Enter」キーをシミュレート
F13 & F15::
    Send {Enter}    ; 「Enter」キーを送信（改行や確定の動作）
    return          ; 処理の終了
```

単押しではa,b,c,dが出力されるようにしています。
そして複数キーでF18やエンターなどが出力されるようにしています。


下記全文
```ahk
; F13を押すと「a」を入力
F13::
    Send a          ; 「a」を送信
    return          ; ホットキーの処理を終了

; F14を押すと「b」を入力
F14::
    Send b          ; 「b」を送信
    return          ; ホットキーの処理を終了

; F15を押すと「c」を入力
F15::
    Send c          ; 「c」を送信
    return          ; ホットキーの処理を終了

; F16を押すと「d」を入力
F16::
    Send d          ; 「d」を送信
    return          ; ホットキーの処理を終了

; F13を押しながらF14を押すと「F17」キーを送信
F13 & F14::
    Send {F17}      ; 仮想キー「F17」を送信（F17対応環境が必要）
    return          ; ホットキーの処理を終了

; F14を押しながらF13を押すと「F18」キーを送信
F14 & F13::
    Send {F18}      ; 仮想キー「F18」を送信（F18対応環境が必要）
    return          ; ホットキーの処理を終了

; F13を押しながらF15を押すと「Enter」キーを送信
F13 & F15::
    Send {Enter}    ; 「Enter」キーを送信（改行や確定）
    return          ; ホットキーの処理を終了

; グローバル変数で選択されたコマンドを保持
global selectedCommand := ""    ; スクリプト全体で使うコマンド保存用変数

; F17を押すとユーザー入力待ちとコマンド選択を開始
F17::
    ; コマンドを格納する連想配列（毎回初期化）
    commandMap := {}            ; 新しい連想配列を作成

    ; commands.csv ファイルを読み込み、コマンドをマッピング
    Loop, Read, commands.csv    ; CSVファイルを1行ずつ読み込む
    {
        line := A_LoopReadLine          ; 現在の行を取得
        StringSplit, fields, line, `,   ; カンマでフィールドを分割
        commandMap[fields1] := fields2  ; キー(fields1)と値(fields2)を連想配列に登録
    }

    ; ユーザーからの入力を受け付け
    InputBox, userInput, 入力待ち, キーワードを入力してください, , , , , , , , ; 入力ボックスを表示
    if ErrorLevel                   ; キャンセルされた場合は終了
        return

    command := commandMap[userInput]    ; 入力に基づくコマンドを取得
    if (command = "")                   ; コマンドが見つからない場合
    {
        Tooltip, 対応するコマンドが見つかりません: %userInput%   ; エラーメッセージを1秒表示
        Sleep, 1000
        ToolTip
        return
    }

    ; コマンドをグローバル変数に保存し、F18ホットキーを有効化
    selectedCommand := command          ; 選択されたコマンドを保存
    Hotkey, F18, ExecuteCommand, On     ; F18を有効化して実行準備
    Tooltip, F18 でコマンドを実行できます: %command%    ; 準備完了を1秒表示
    Sleep, 1000
    ToolTip
    return

; F18を押すと保存されたコマンドを実行
F18::
ExecuteCommand:             ; F18ホットキーのラベル
    if (selectedCommand = "")   ; コマンドが未設定の場合
    {
        MsgBox, コマンドが設定されていません。  ; エラーメッセージを表示
        return
    }
    RunCommand(selectedCommand) ; 保存されたコマンドを実行
    ; Hotkey, F18, Off          ; 実行後にF18を無効化（コメントアウト済み）
    return

; コマンドを実行する関数
RunCommand(cmd) {                       ; 引数cmdを受け取る関数
    StringSplit, commands, cmd, `;      ; コマンドをセミコロンで分割
    Loop, %commands0%                   ; 分割されたコマンドの数だけ繰り返し
    {
        currentCmd := Trim(commands%A_Index%)   ; 現在のコマンドを取得（空白除去）
        if (SubStr(currentCmd, 1, 5) = "Send ") ; コマンドが「Send」で始まる場合
        {
            action := Trim(SubStr(currentCmd, 6))   ; Send以降の部分を取得
            if (RegExMatch(action, "^{.*}$"))       ; 特殊キー形式（{...}）の場合
            {
                StringReplace, action, action, {, , All ; { を削除
                StringReplace, action, action, }, , All ; } を削除
                ; 特殊キーの場合は Send、それ以外は SendRaw で処理
                if (action = "Home" || action = "End" || action = "Enter" || action = "Tab" || action = "Up" || action = "Down")
                {
                    Send {%action%}     ; 特殊キーを送信
                }
                else
                {
                    SendRaw %action%    ; 通常文字列をそのまま送信
                }
            }
            else
            {
                Send %action%   ; キーコマンドとして送信
            }
        }
        else if (SubStr(currentCmd, 1, 4) = "Run ") ; コマンドが「Run」で始まる場合
        {
            action := SubStr(currentCmd, 5) ; Run以降の部分を取得
            Run %action%                    ; プログラムやファイルを起動
        }
        else
        {
            SendRaw %currentCmd%    ; そのまま文字列として送信
        }
        Sleep, 200      ; コマンド間に200ms待機（安定性確保）
    }
}
```
このスクリプトと同じ階層フォルダにcommands.csvというファイルを用意します
中身はこんな感じです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/2097f2db-e061-4247-b07b-0df343c3e2ce.png)

このスクリプトをダブルクリックで実行したあとで、F17を押して起動すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/e5896abf-d921-43d5-82b4-ebdb02543762.png)
ここに例えば
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/30de19c3-f37c-49d9-879d-88de107e9c7e.png)
を入力し、EnterなどでOKを押すと次、F18を押すことでHINATAHUGUと出力されます

このCSVファイルは実行の度に読み込みに行きますので、必要に応じて動的に変えていくことが可能となります

これをさらに拡張して一旦の完成形としてます

```ahk
; F13を押すと「a」を入力
F13::
    Send a          ; 「a」を送信
    return          ; 処理終了

; F14を押すと「b」を入力
F14::
    Send b          ; 「b」を送信
    return          ; 処理終了

; F15を押すと「c」を入力
F15::
    Send c          ; 「c」を送信
    return          ; 処理終了

; F16を押すと「d」を入力
F16::
    Send d          ; 「d」を送信
    return          ; 処理終了

; F13を押しながらF14を押すと「F17」を送信
F13 & F14::
    Send {F17}      ; 仮想キー「F17」を送信（対応環境必要）
    return          ; 処理終了

; F14を押しながらF13を押すと「F18」を送信
F14 & F13::
    Send {F18}      ; 仮想キー「F18」を送信（対応環境必要）
    return          ; 処理終了

; F14を押しながらF15を押すと「F19」を送信
F14 & F15::
    Send {F19}      ; 仮想キー「F19」を送信（対応環境必要）
    return          ; 処理終了

; F15を押しながらF14を押すと「F20」を送信
F15 & F14::
    Send {F20}      ; 仮想キー「F20」を送信（対応環境必要）
    return          ; 処理終了

; F15を押しながらF16を押すと「F21」を送信
F15 & F16::
    Send {F21}      ; 仮想キー「F21」を送信（対応環境必要）
    return          ; 処理終了

; F16を押しながらF15を押すと「F22」を送信
F16 & F15::
    Send {F22}      ; 仮想キー「F22」を送信（対応環境必要）
    return          ; 処理終了

; F13を押しながらF16を押すと「Enter」を送信
F13 & F16::
    Send {Enter}    ; 「Enter」を送信（改行や確定）
    return          ; 処理終了

; グローバル変数で選択されたコマンドを保持
global selectedCommand := ""    ; コマンド保存用変数（F17-F18用）
global selectedCommand1 := ""   ; コマンド保存用変数（F19-F20用）
global selectedCommand2 := ""   ; コマンド保存用変数（F21-F22用）

; F17を押すとコマンド入力と選択を開始
F17::
    commandMap := {}            ; 連想配列を初期化
    Loop, Read, commands.csv    ; CSVを読み込み
    {
        line := A_LoopReadLine          ; 行を取得
        StringSplit, fields, line, `,   ; カンマで分割
        commandMap[fields1] := fields2  ; キー・値を登録
    }
    InputBox, userInput, 入力待ち, キーワードを入力してください, , , , , , , , ; 入力受付
    if ErrorLevel                   ; キャンセル時終了
        return
    command := commandMap[userInput]    ; コマンド取得
    if (command = "")                   ; コマンド未発見時
    {
        Tooltip, 対応するコマンドが見つかりません: %userInput%   ; エラー表示（1秒）
        Sleep, 1000
        ToolTip
        return
    }
    selectedCommand := command          ; コマンドを保存
    Hotkey, F18, ExecuteCommand, On     ; F18を有効化
    Tooltip, F18 でコマンドを実行できます: %command%    ; 通知（1秒）
    Sleep, 1000
    ToolTip
    return

; F18を押すと選択されたコマンドを実行
F18::
ExecuteCommand:             ; F18の実行ラベル
    if (selectedCommand = "")   ; コマンド未設定時
    {
        MsgBox, コマンドが設定されていません。  ; エラー表示
        return
    }
    RunCommand(selectedCommand) ; コマンド実行
    ; Hotkey, F18, Off          ; F18無効化（コメントアウト）
    return

; コマンドを実行する関数（F17-F18用）
RunCommand(cmd) {
    StringSplit, commands, cmd, `;      ; セミコロンで分割
    Loop, %commands0%                   ; コマンド数分繰り返し
    {
        currentCmd := Trim(commands%A_Index%)   ; コマンド取得（空白除去）
        if (SubStr(currentCmd, 1, 5) = "Send ") ; Sendコマンドの場合
        {
            action := Trim(SubStr(currentCmd, 6))   ; Send以降を取得
            if (RegExMatch(action, "^{.*}$"))       ; 特殊キー形式の場合
            {
                StringReplace, action, action, {, , All ; { を削除
                StringReplace, action, action, }, , All ; } を削除
                if (action = "Home" || action = "End" || action = "Enter" || action = "Tab" || action = "Up" || action = "Down")
                {
                    Send {%action%}     ; 特殊キー送信
                }
                else
                {
                    SendRaw %action%    ; 通常文字列送信
                }
            }
            else
            {
                Send %action%   ; キーコマンド送信
            }
        }
        else if (SubStr(currentCmd, 1, 4) = "Run ") ; Runコマンドの場合
        {
            action := SubStr(currentCmd, 5) ; Run以降を取得
            Run %action%                    ; 実行
        }
        else
        {
            SendRaw %currentCmd%    ; そのまま送信
        }
        Sleep, 200      ; コマンド間待機（200ms）
    }
}

; F19を押すとコマンド入力と選択を開始
F19::
    commandMap1 := {}           ; 連想配列を初期化
    Loop, Read, commands.csv    ; CSVを読み込み
    {
        line := A_LoopReadLine          ; 行を取得
        StringSplit, fields, line, `,   ; カンマで分割
        commandMap1[fields1] := fields2 ; キー・値を登録
    }
    InputBox, userInput, 入力待ち, キーワードを入力してください, , , , , , , , ; 入力受付
    if ErrorLevel                   ; キャンセル時終了
        return
    command := commandMap1[userInput]   ; コマンド取得
    if (command = "")                   ; コマンド未発見時
    {
        Tooltip, 対応するコマンドが見つかりません: %userInput%   ; エラー表示（1秒）
        Sleep, 1000
        ToolTip
        return
    }
    selectedCommand1 := command         ; コマンドを保存
    Hotkey, F20, ExecuteCommand1, On    ; F20を有効化
    Tooltip, F20 でコマンドを実行できます: %command%    ; 通知（1秒）
    Sleep, 1000
    ToolTip
    return

; F20を押すと選択されたコマンドを実行
F20::
ExecuteCommand1:            ; F20の実行ラベル
    if (selectedCommand1 = "")  ; コマンド未設定時
    {
        MsgBox, コマンドが設定されていません。  ; エラー表示
        return
    }
    RunCommand1(selectedCommand1)   ; コマンド実行
    ; Hotkey, F20, Off          ; F20無効化（コメントアウト）
    return

; コマンドを実行する関数（F19-F20用）
RunCommand1(cmd) {
    StringSplit, commands, cmd, `;      ; セミコロンで分割
    Loop, %commands0%                   ; コマンド数分繰り返し
    {
        currentCmd := Trim(commands%A_Index%)   ; コマンド取得（空白除去）
        if (SubStr(currentCmd, 1, 5) = "Send ") ; Sendコマンドの場合
        {
            action := Trim(SubStr(currentCmd, 6))   ; Send以降を取得
            if (RegExMatch(action, "^{.*}$"))       ; 特殊キー形式の場合
            {
                StringReplace, action, action, {, , All ; { を削除
                StringReplace, action, action, }, , All ; } を削除
                if (action = "Home" || action = "End" || action = "Enter" || action = "Tab" || action = "Up" || action = "Down")
                {
                    Send {%action%}     ; 特殊キー送信
                }
                else
                {
                    SendRaw %action%    ; 通常文字列送信
                }
            }
            else
            {
                Send %action%   ; キーコマンド送信
            }
        }
        else if (SubStr(currentCmd, 1, 4) = "Run ") ; Runコマンドの場合
        {
            action := SubStr(currentCmd, 5) ; Run以降を取得
            Run %action%                    ; 実行
        }
        else
        {
            SendRaw %currentCmd%    ; そのまま送信
        }
        Sleep, 200      ; コマンド間待機（200ms）
    }
}

; F21を押すとコマンド入力と選択を開始
F21::
    commandMap2 := {}           ; 連想配列を初期化
    Loop, Read, commands.csv    ; CSVを読み込み
    {
        line := A_LoopReadLine          ; 行を取得
        StringSplit, fields, line, `,   ; カンマで分割
        commandMap2[fields1] := fields2 ; キー・値を登録
    }
    InputBox, userInput, 入力待ち, キーワードを入力してください, , , , , , , , ; 入力受付
    if ErrorLevel                   ; キャンセル時終了
        return
    command := commandMap2[userInput]   ; コマンド取得
    if (command = "")                   ; コマンド未発見時
    {
        Tooltip, 対応するコマンドが見つかりません: %userInput%   ; エラー表示（1秒）
        Sleep, 1000
        ToolTip
        return
    }
    selectedCommand2 := command         ; コマンドを保存
    Hotkey, F22, ExecuteCommand2, On    ; F22有効化
    Tooltip, F22 でコマンドを実行できます: %command%    ; 通知（1秒）
    Sleep, 1000
    ToolTip
    return

; F22を押すと選択されたコマンドを実行
F22::
ExecuteCommand2:            ; F22の実行ラベル
    if (selectedCommand2 = "")  ; コマンド未設定時
    {
        MsgBox, コマンドが設定されていません。  ; エラー表示
        return
    }
    RunCommand2(selectedCommand2)   ; コマンド実行
    ; Hotkey, F22, Off          ; F22無効化
    return

; コマンドを実行する関数（F21-F22用）
RunCommand2(cmd) {
    StringSplit, commands, cmd, `;      ; セミコロンで分割
    Loop, %commands0%                   ; コマンド数分繰り返し
    {
        currentCmd := Trim(commands%A_Index%)   ; コマンド取得（空白除去）
        if (SubStr(currentCmd, 1, 5) = "Send ") ; Sendコマンドの場合
        {
            action := Trim(SubStr(currentCmd, 6))   ; Send以降を取得
            if (RegExMatch(action, "^{.*}$"))       ; 特殊キー形式の場合
            {
                StringReplace, action, action, {, , All ; { を削除
                StringReplace, action, action, }, , All ; } を削除
                if (action = "Home" || action = "End" || action = "Enter" || action = "Tab" || action = "Up" || action = "Down")
                {
                    Send {%action%}     ; 特殊キー送信
                }
                else
                {
                    SendRaw %action%    ; 通常文字列送信
                }
            }
            else
            {
                Send %action%   ; キーコマンド送信
            }
        }
        else if (SubStr(currentCmd, 1, 4) = "Run ") ; Runコマンドの場合
        {
            action := SubStr(currentCmd, 5) ; Run以降を取得
            Run %action%                    ; 実行
        }
        else
        {
            SendRaw %currentCmd%    ; そのまま送信
        }
        Sleep, 200      ; コマンド間待機（200ms）
    }
}
```
# 動作イメージ
![初心者がBlenderで遊ぶところ (16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/2e724e8d-03f3-42f8-b714-8752c8522dcc.png)
こんな感じですかね
今回は隣同士の組み合わせで3つにしましたが、両端同士も含めて4つまでは少なくとも延長で可能です

先ほどのCSVの中身については文字列だけではなくて、AHK文法に則って書けばCtrl+Cなどの実行もできます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2723011/7dc3fe85-2b61-44aa-b4d6-f89457de2964.png)
例えばですが、こんな感じで用意をしておけばいつでも呼び出せるコマンドせっとになります

会社でもこのちょい足しキーボードにここまでのことをするのか、とかよく言われますが
ここまで来るともはやただのロマンですね(笑)

なにかのお役に立てれば幸いです。
商品が気になった方はこちらから(笑)

https://amzn.to/3Xmk1UY