[AutoHotKey]配列をリストビューで選択して返す関数


- [\[AutoHotKey\]ある正規表現パターンにマッチする全ての部分を返す関数 \- Qiita](https://qiita.com/catfist/items/db934bf8ca1a84feba56)

↑とちょっと関連。

![](https://i.imgur.com/0TnR2ZZ.jpg)

## 動機
パターンマッチで抜き出したキー文字列をGUIで選択して、次の処理に渡したい。

## 実装
ぶっちゃけ`Return`の位置についてひたすら答えを求め続ける旅路だった。

```ListViewSelection.ahk
ListViewSelection(Array, title="項目を選択", w=500, h=600, fs=13, ff="cica")
{
    Gui, Font, s%fs%, %ff%
    Gui, Add, ListView, -Multi Grid w%w% h%h% gMyListView AltSubmit HwndMyGuiHwnd, %title%
    Gui, Add, Button, gGuiClose, 終了(&X)

    ; パラメータ1の配列からループ
    for index, key in Array
    {
        LV_Add("", key)
    }

    ; GUIを表示
    Gui, Show

    WinWaitClose, ahk_id %MyGuiHwnd% ; GUIウィンドウが閉じるまで待つ
    Return, RowText ; <<<<<<<<<<< ココ！　ここな！！！

    MyListView:
        If (A_GuiEvent = "DoubleClick" or A_GuiEvent = "Normal")
        {
            If (A_EventInfo > 0) ; 項目をクリックしたら
            {
                LV_GetText(RowText, A_EventInfo) ; 選択項目を変数に格納
                GoTo, GuiClose ; GUIの破棄に飛ぶ
            }
            Else ; 項目のないとこをクリックしたら何もしない
                Return
        }
        Return
    GuiEscape: ; Escキーを押した時
    GuiClose: ; 「×」ボタン等で閉じたとき
        Gui, Destroy ; GUIウィンドウを破棄
        ; メインルーチンに復帰
        Return
}
```

### ポイント1：選択完了まで処理を保留する
`Gui, Show`でGUIを表示した後、スクリプトはフツーに次の行へ進んでいく。
次の処理はGUIで項目を選択してから行いたいわけで、それでは困る。

これは、GUIウィンドウのウィンドウハンドルを取得し、そのウィンドウが閉じるのを待つことで解決した。

`Gui, Add`の際に`Hwnd<変数名>`オプションを付与することで、生成したGUIウィンドウのウィンドウハンドルがその変数に格納される。
これを元に`WinWaitClose`のウィンドウ指定（`ahk_id Hwnd<変数名>`）を行うことで、GUIの始末が付くまで処理を保留できる。

### ポイント2：アイテム選択の実装
正直このへん未だに理解がビミョーなのだが。

まず、`Gui, Add`に`g<ラベル名>`オプションを付与することで、GUIウィンドウに対してなんらかの操作を行った際に発火するイベントを実装できる。

さらに`AltSubmit`オプションを付与することで、`Normal`（普通の左クリック）を発火条件として利用できるようになる。

`g<ラベル名>`オプションで指定したラベルでは、以下のような情報が利用できる。
なんか、できた。

- `A_GuiEvent`で、行われた操作
- `%A_EventInfo%`で、操作されたアイテムの行番号
- ↑から`LV_GetText()`関数経由で、操作されたアイテムのテキスト（この場合は`RowText`変数に格納）

よって、`If`で`A_GuiEvent`変数を判定することで操作に対する処理を実装できる。
また、`%A_EventInfo%`変数を判定することで、どのアイテムも操作対象になっていない場合（空欄をクリックした場合など）を排除できる。

そして、`RowText`に格納されているのが、最終的に得たいもの＝配列に格納された候補の中から選択された項目であり、これを`Return`すればよい。

### ポイント3：メインルーチンの`WinWaitClose`の後に`Return`する
GUIの終了処理が複雑でだいぶ混乱した。

まず、GUIウィンドウの右上の「×」ボタンをクリックしても、「ALt+F4」を押しても、それだけではGUIウィンドウは閉じられない。
このような「ウィンドウを閉じる操作」を行うと、`GuiClose`ラベルのサブルーチンが実行される。ここで`Gui, Destroy`を実行することで、初めてGUIウィンドウが閉じられる（破棄される）ことになる。
これは、`Gui, Add, Button`で実装されている「終了(&X)」ボタンも同様。コードの単純化のため、共通で`GuiClose`ラベルに飛ばしている。

（逆に言えば、スクリプト内のどこだろうと（関数外だろうと）`Gui, Destroy`を実行すればGUIウィンドウは破棄される。）

そして、`GuiClose`ラベルのサブルーチンが（`Return`で）終了すると、スクリプトはメインルーチンに復帰する。
（この`Return`は関数内サブルーチンのそれであって、関数の最終的な`Return`ではないので、関数の戻り値にはならない。）

このとき、GUIウィンドウは破棄されているので、`WinWaitClose`が終了し、その次の行に処理が進む。
よって、`WinWaitClose`の下の処理で`Return, RowText`を行うことで、`RowText`変数を関数の戻り値として得ることができる。

## 使い方
### 例1
コンパイルして使う場合。
パラメータからひとつを選択して表示する。

```ahk:ListViewSelection.exe
; A_Argsはコマンドラインパラメータが格納される配列オブジェクト

MsgBox, , %A_scriptname%, % ListViewSelection(A_Args, "キーを選択（Escでキャンセル）")
```

### 例2
めっちゃ深い階層のファイルパスから、ディレクトリ名を抜き出してコピーする。

ようやく冒頭の画像のやつ

```ahk:ListViewSelection.ahk
File = C:\Users\<ユーザー名>\.vscode\extensions\vscodevim.vim-0.14.0\node_modules\async\all.js
Array := Object()
Loop, Parse, File, \
{
    Array.insert(A_LoopField)
}
Clipboard := ListViewSelection(Array, "キーを選択（Escでキャンセル）")
```

とにかく配列にすればなんでも見て選べて、選んだものをどうにかできる。

## 実装2：Enterキーの捕捉
実際使ってみると、キーボードで決定したくなった。

`AltSubmit`オプションでキー入力を捕捉する`K`イベントも利用できるのだが、これだとEnterキーが補足できなかった。

Enterキーの入力自体は、`Gui, Add, Button`に`Default`オプションを与えれば捕捉できる。
そこからラベルに飛ばして処理することになるが、リストビューからのイベントではないので、この処理では行番号等を参照できない。
よって、方向キーやスペースキー等によるアイテム選択時点で変数に格納しておく必要がある。

```ListViewSelection.ahk
ListViewSelection(Array, title="項目を選択", w=500, h=600, fs=13, ff="cica")
{
    Gui, Font, s%fs%, %ff%
    Gui, Add, ListView, -Multi Grid w%w% h%h% gMyListView AltSubmit HwndMyGuiHwnd, %title%
    Gui, Add, Button, gGuiClose, 終了(&X)
    Gui, Add, Button, X+10 gDecision Default, 決定(&D)

    ; パラメータ1の配列からループ
    for index, key in Array
    {
        LV_Add("", key)
    }

    ; GUIを表示
    Gui, Show

    ; Escでキャンセル
    WinWaitClose, ahk_id %MyGuiHwnd%
    Return, RowText

    MyListView:
        If (A_GuiEvent = "DoubleClick")
        {
            If (A_EventInfo > 0) ; 項目をダブルクリックしたら
            {
                GoTo, GuiClose ; GUIの破棄に飛ぶ
            }
            Else ; 項目のないとこをクリックしたら何もしない
                Return
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "S", true))
        {
            LV_GetText(RowText, A_EventInfo) ; 選択されたアイテムを変数に格納
            Return
        }
        ; Enterキーは捕捉できない
        ; If (A_GuiEvent = "K" and InStr(A_EventInfo, "13", False))
        ; {
        ;     GoTo, GuiClose
        ;     Return
        ; }
        Return
    GuiEscape: ; GuiがアクティブでEscキーを押した時
    GuiClose: ; 「×」ボタン等で閉じたとき
        RowText := Blank ; 変数を空にする
    Decision:
        Gui, Destroy
        ; メインルーチンに復帰
        Return
}
```

## 実装3：複数選択対応
複数選択もしたくなった。

この場合、シングルクリックで決定されては困るのでそれは当然やめるとして。

問題は、ダブルクリックなりEnterなりで決定のイベントが発火する時、**その時選択されているすべての項目**を取得する方法がないということ。

ということは、選択する時点で項目の値を取得し、選択を解除したら削除する必要がある。

そこから[\[AutoHotKey\]インデックスが連番でない配列のinsert・removeについて \- Qiita](https://qiita.com/catfist/items/b6a6cbf6cf133dce21f2)に繋がったわけ。

というわけで、選択した全ての項目の値を配列で返すバージョン。

```ListViewSelection.ahk
ListViewSelection(Array, title="項目を選択", w=500, h=600, fs=13, ff="cica")
{
    Gui, Font, s%fs%, %ff%
    Gui, Add, ListView, Grid w%w% h%h% gMyListView AltSubmit HwndMyGuiHwnd, %title%
    Gui, Add, Button, gGuiClose, 終了(&X)
    Gui, Add, Button, X+10 gDecision Default, 決定(&D)

    Selection := object()

    ; パラメータ1の配列からループ
    for index, key in Array
    {
        LV_Add("", key)
    }

    ; GUIを表示
    Gui, Show

    WinWaitClose, ahk_id %MyGuiHwnd%

    ; 文字列キーを数値インデックスに直す
    for index, element in Selection
    {
        Selection.remove(index)
        StringTrimLeft, index, index, 1
        Selection.insert(index, element)
    }
    Return, Selection

    MyListView:
        If (A_GuiEvent = "DoubleClick")
        {
            GoTo, GuiClose
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "S", true))
        {
            LV_GetText(RowText, A_EventInfo)
            Selection.insert("r" . A_EventInfo, RowText)
            Return
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "s", true))
        {
            Selection.remove("r" . A_EventInfo)
            Return
        }
        Return
    GuiEscape:
    GuiClose:
        Selection := object()
    Decision:
        Gui, Destroy
        ; メインルーチンに復帰
        Return
}
```

## 実装4：単複両対応
`Mode`パラメータに応じてリストビューの`Multi`オプションを切り替え。

シングルモードの場合、「値が1つの配列」だと扱いづらいので、変数にして返す。

```ListViewSelection.ahk
ListViewSelection(Mode, Array, title="項目を選択", w=500, h=600, fs=13, ff="cica")
{
    If (Mode = "Single")
        MultiOption = -Multi
    Gui, Font, s%fs%, %ff%
    Gui, Add, ListView, %MultiOption% Grid w%w% h%h% gMyListView AltSubmit HwndMyGuiHwnd, %title%
    Gui, Add, Button, gGuiClose, 終了(&X)
    Gui, Add, Button, X+10 gDecision Default, 決定(&D)

    Selection := object()

    ; パラメータ1の配列からループ
    for index, key in Array
    {
        LV_Add("", key)
    }

    ; GUIを表示
    Gui, Show

    WinWaitClose, ahk_id %MyGuiHwnd%

    ; 文字列キーを数値インデックスに直す
    for index, element in Selection
    {
        if (Mode = "Single") ; シングルモードの場合、配列を変数に変更
        {
            Selection := element
            Break
        }
        ; if (A_Index = 1)
        ;     Array := Object()
        Selection.remove(index)
        StringTrimLeft, index, index, 1
        Selection.insert(index, element)
        ; Selection.insert(a_index, element)
    }
    Return, Selection

    MyListView:
        If (A_GuiEvent = "DoubleClick")
        {
            GoTo, GuiClose
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "S", true))
        {
            LV_GetText(RowText, A_EventInfo)
            Selection.insert("r" . A_EventInfo, RowText)
            Return
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "s", true))
        {
            Selection.remove("r" . A_EventInfo)
            Return
        }
        Return
    GuiEscape:
    GuiClose:
        Selection := object()
    Decision:
        Gui, Destroy
        ; メインルーチンに復帰
        Return
}
```

## 実装5：2列化で数字アクセラレーターキーによる選択に対応
![スクリーンショット](https://i.imgur.com/mvCCOWr.jpg)

1列目に数字とかを入れておけば、該当するキーの入力で選択できてなおよい。

ともなって表示を色々調整。

```ListViewSelection.ahk
ListViewSelection(Mode, Array, title="項目を選択", w=500, h="r15", fs=11, ff="CocaE", fs2=13, ff2="meiryo") {
    If (Mode = "Single")
        MultiOption = -Multi
    ; Gui, Font, s%fs% bold, "Cica"
    Gui, Font, s%fs2%, %ff2%
    Gui, Add, Text, w%w% center, %title%
    Gui, Font, s%fs% Norm, %ff%
    If not (InStr(h, "r"))
        h = h%h%
    Gui, Add, ListView, %MultiOption% Grid -Hdr w%w% %h% gMyListView AltSubmit HwndMyGuiHwnd, No|値 ; 見出しは表示しないが見出し内容を入力しないと列自体ができない
    Gui, Add, Button, gGuiClose, 終了(&X)
    Gui, Add, Button, X+10 gDecision Default, 決定(&D)
    Selection := object()

    ; パラメータ1の配列からループ
    for index, key in Array
    {
        LV_Add(a_index, index, key)
    }

    LV_ModifyCol(1, "AutoHdr, Right")
    LV_ModifyCol(2, "AutoHdr")

    ; GUIを表示
    Gui, Show

    WinWaitClose, ahk_id %MyGuiHwnd%
    ; 文字列キーを数値インデックスに直す
    for index, element in Selection
    {
        if (Mode = "Single") ; シングルモードの場合、配列を変数に変更
        {
            Selection := element
            Break
        }
        ; if (A_Index = 1)
        ;     Array := Object()
        Selection.remove(index)
        StringTrimLeft, index, index, 1
        Selection.insert(index, element)
        ; Selection.insert(a_index, element)
    }
    Return, Selection

    MyListView:
        If (A_GuiEvent = "DoubleClick")
        {
            GoTo, GuiClose
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "S", true))
        {
            LV_GetText(RowText, A_EventInfo, 2)
            LV_GetText(RowNum, A_EventInfo, 1)
            Selection.insert("r" . RowNum, RowText)
            Return
        }
        If (A_GuiEvent = "I" and InStr(ErrorLevel, "s", true))
        {
            LV_GetText(RowNum, A_EventInfo, 1)
            Selection.remove("r" . RowNum)
            Return
        }
        Return
    GuiEscape:
    GuiClose:
        Selection := object()
        Gui, Destroy
        Return
    Decision:
        for, key, value in Selection
        {
            Gui, Destroy
            Break
        }
        Return
}
```