AutoHotkeyで指定ウィンドウごとに独自マクロを設定・実行する

特定のアプリにて同じ入力を繰り返し行った際、その入力を自動化したくなる場面がありますが、その自動入力をAutoHotkey(以降AHK)スクリプト内の独自マクロとして記述し、実行出来るようにしました。
指定したウィンドウを対象に、Send・Sleep・Copy・Paste・文字列の挿入を実行する事が出来ます。

**11/5: コピー・ペーストコマンドを追加しました。**

マクロの編集画面
![macro_edit.png](https://qiita-image-store.s3.amazonaws.com/0/51728/39289ca9-85c9-10fd-887b-820b9f77e053.png)

入力欄がマクロの記述先、Backgroundチェックボックスはマクロ実行後にアクティブウィンドウをもとに戻すか、Shift+Insert Pasteチェックボックスは文字列ペーストをShift+Insertで行うか(デフォルトはCtrl+v)の設定です。

本記事のスクリプトは[最後](#スクリプト)に記載しています。

# 使い方

1. 任意のウィンドウをアクティブ状態にし、独自マクロの編集画面を起動します。
2. 独自マクロを記述後、OKボタンを押します。
3. この状態で、別のウィンドウをアクティブにしていても1.のウィンドウに対して独自マクロの操作が実行されるようになります。

# 使用例・デモ

これだけだとピンとこないので幾つか使用例を載せておきます。

## その1(コマンドプロンプト)
コマンドプロンプトにてC言語のコンパイル・実行する場合は

```
>gcc main.c -o a.exe && a
```

と入力・実行出来ますが、これをマクロ内に文字列として記述し、任意のコマンドプロンプトを対象にして実行させる事が出来ます。

マクロ内容(再掲)
![macro_edit.png](https://qiita-image-store.s3.amazonaws.com/0/51728/39289ca9-85c9-10fd-887b-820b9f77e053.png)

デモ(コンパイル・実行)
![gcc_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/5d813922-8b26-9145-2192-43caafa25418.gif)


## その2(メモ帳)

メモ帳にて、メニューの「日付と時刻」を組み合わせてみます。

マクロ内容
![macro_notepad_edit.png](https://qiita-image-store.s3.amazonaws.com/0/51728/e035d8b2-8ee2-bcda-b7c2-de517efb3055.png)
「日付と時刻」のキーボード選択はAlt+e、dキーで出来ます。

デモ(日付と時刻の入力)
![notepad_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/ce0a52c9-e7e9-bddd-aa13-eec501b050f1.gif)

また、日付と時刻取得ツールとして利用する事も出来ます。

マクロ内容
![macro_time.png](https://qiita-image-store.s3.amazonaws.com/0/51728/4d710fbd-81fb-cc8a-fc6d-eb16fe471109.png)


デモ(日付と時刻取得ツールとして)
![demo_time.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/9e17895d-55f1-6d2c-8342-33bd74d0d9b2.gif)


ペーストだけをして、コピー内容のストックとして利用してみます。

マクロ内容
![macro_paste.png](https://qiita-image-store.s3.amazonaws.com/0/51728/983f1c4d-6c90-df5b-962b-954886a1d316.png)

デモ(コピー内容ストック)
![demo_paste.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/f65a41ad-7a9d-2610-828f-27d8f96164e4.gif)


## その3(電卓)

計算結果を受け取ってみます。

マクロ内容
![macro_calc.png](https://qiita-image-store.s3.amazonaws.com/0/51728/8b2bb83f-1031-f02f-2469-4476506b3ac4.png)

デモ(計算結果の取得)
![demo_calc.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/1494121e-6bdb-09a7-c92c-b4493a805de7.gif)

# スクリプト

掲載ソースでは無変換 & ファンクションキーに設定しています、Ctrlキーの有無で編集、実行を切り替え出来るようにしています。
また、独自マクロはスクリプトと同じフォルダに「minimacro」フォルダを掘り、その中に保存されるようになっています。

これらが気に入らなければカスタマイズして下さい。

```c:AutoHotkey.ahk
; ミニマクロ
sc07B & F1:: mmEditOrRun(macids, 1)
sc07B & F2:: mmEditOrRun(macids, 2)
sc07B & F3:: mmEditOrRun(macids, 3)
{
  ; 編集/実行
  mmEditOrRun(ByRef ids, idx)
  {
    ; initialize
    global g_mm_checkBackgrounds
    global g_mm_checkShiftInsertPastes

    g_mm_checkBackgrounds := (g_mm_checkBackgrounds) ? g_mm_checkBackgrounds : {}
    g_mm_checkShiftInsertPastes := (g_mm_checkShiftInsertPastes) ? g_mm_checkShiftInsertPastes : {}
    ids := (ids) ? ids : {}

    if GetKeyState("ctrl", "P") {
      mmEdit(ids, idx)
    } else {
      mmRun(ids, idx)
    }
  }

  g_mm_checkBackgrounds := {}
  g_mm_checkShiftInsertPastes := {}
  g_mm_guiIndex := -1
  g_mm_editMacro := ""

  ; 編集
  mmEdit(ByRef ids, idx)
  {
    global g_mm_checkBackgrounds
    global g_mm_checkShiftInsertPastes
    global g_mm_guiIndex
    global g_mm_editMacro

    g_mm_guiIndex := idx
    g_mm_editMacro := mmRead(idx)

    ; 対象ウィンドウを記憶
    WinGet, id, ID, A
    ids[g_mm_guiIndex] := id

    background := g_mm_checkBackgrounds.HasKey(idx) ? g_mm_checkBackgrounds[idx] : 1
    checkedBG := (background) ? "Checked" : ""

    paste := g_mm_checkShiftInsertPastes.HasKey(idx) ? g_mm_checkShiftInsertPastes[idx] : 0
    checkedPaste := (paste) ? "Checked" : ""

    ; 編集画面
    Gui, Add, Edit, W300 H70 vg_mm_editMacro, %g_mm_editMacro%
    Gui, Add, Checkbox, vcheckBackground %checkedBG%, Background(&B)
    Gui, Add, Checkbox, vcheckShiftInsertPaste %checkedPaste%, Shift+Insert Paste(&P)
    Gui, Add, Button, W80 X70, &OK
    Gui, Add, Button, W80 X+0, &Cancel
    Gui, Show, , MiniMacro (%g_mm_guiIndex%)
  }
  ButtonOK:
    Gui, Submit
    ; ミニマクロ、設定の保存
    mmWrite(g_mm_guiIndex, g_mm_editMacro)
    g_mm_checkBackgrounds[g_mm_guiIndex] := checkBackground
    g_mm_checkShiftInsertPastes[g_mm_guiIndex] := checkShiftInsertPaste
  ButtonCancel:
  GuiClose:
  GuiEscape:
    Gui, Destroy
    return

  ; 実行
  mmRun(ByRef ids, idx)
  {
    global g_mm_checkBackgrounds
    global g_mm_checkShiftInsertPastes

    if (!ids.HasKey(idx)) {
      MsgBox, 0x10, ,Target Window is none
      return
    }

    id := ids[idx]
    background := g_mm_checkBackgrounds.HasKey(idx) ? g_mm_checkBackgrounds[idx] : 1
    shiftInsertPaste := g_mm_checkShiftInsertPastes.HasKey(idx) ? g_mm_checkShiftInsertPastes[idx] : 0

    clip_bak := ClipboardAll
    WinGet, win_bak, ID, A

    ; 対象ウィンドウをアクティブにし
    WinActivate, ahk_id %id%
    ; ミニマクロ実行
    for _, line in StrSplit(mmRead(idx), "`n")
    {
      line := Trim(line)

      if (InStr(line, "Sleep,") == 1) {
        msec := Trim(SubStr(line, 7))
        Sleep, %msec%
      } else if (InStr(line, "Send,") == 1) {
        keys := Trim(SubStr(line, 6))
        Send, %keys%
      } else if (InStr(line, "Copy,") == 1) {
        Clipboard = ; Empty the clipcboard
        Send, ^c
        copyWaitWithThrow()
        clip_bak := ClipboardAll
      } else if (InStr(line, "Paste,") == 1) {
        if (shiftInsertPaste) {
          Send, +{Blind}{Insert}
        } else {
          Send, ^v
        }
        pasteWaitWithThrow()
     } else {
        Clipboard := line
        ; 貼り付けEnter
        if (shiftInsertPaste) {
          Send, +{Blind}{Insert}
        } else {
          Send, ^v
        }
        Send, {Blind}{Enter}
        pasteWaitWithThrow()
        Clipboard := clip_bak
      }
    }

    ; 復元
    if (background) {
      WinActivate, ahk_id %win_bak%
    }
  }

  ; クリップボードのペースト完了を待つ
  pasteWaitWithThrow()
  {
    local timeout = 2000
    local h
    local tic := a_tickcount
    Sleep, 10
    while ((h := DllCall("GetOpenClipboardWindow", "Ptr")) && a_tickcount - tic < timeout) {
      Sleep, 10
    }
    Sleep, 10

    if (!!h) {
      throw "The attempt to paste text from the clipboard failed."
    }
  }
  ; クリップボードのコピー完了を待つ
  copyWaitWithThrow()
  {
    ClipWait, 2
    if (ErrorLevel <> 0) {
      throw "The attempt to copy text onto the clipboard failed."
    }
  }

  ; ミニマクロ読み込み
  mmRead(idx)
  {
    dir := A_ScriptDir . "\minimacro"
    filename := dir . "\minimacro" . idx . ".txt"

    macro := ""
    if (FileExist(filename)) {
      FileRead, macro, *t %filename%
    }
    return macro
  }
  ; ミニマクロ書き出し
  mmWrite(idx, macro)
  {
    dir := A_ScriptDir . "\minimacro"
    if (!FileExist(dir)) {
      FileCreateDir, %dir%
    }
    filename := dir . "\minimacro" . idx . ".txt"

    file := FileOpen(filename, "w `n")
    file.Write(macro)
    file.Close()
  }
}
```

# 余談(コピー・ペーストについて)

コピーの完了待ちは、AHK組み込みのClipWait関数で待つようにしましょう、その際は一度クリップボードを空にしないと上手く待ってくれません。
ペーストの完了待ちは、Win32APIのGetOpenClipboardWindows関数でクリップボードを操作しているアプリの存在をチェックし、無くなるまで待機をすれば上手く動作します。

# 参考

- [AutoHotkeyJpリファレンス - ClipWait](https://sites.google.com/site/autohotkeyjp/reference/commands/ClipWait)
- [AutoHotkeyフォーラム - Clipboard: 'PasteWait': wait for paste to complete](https://autohotkey.com/boards/viewtopic.php?f=5&t=37209)