↑とちょっと関連。
動機
パターンマッチで抜き出したキー文字列をGUIで選択して、次の処理に渡したい。
実装
ぶっちゃけReturn
の位置についてひたすら答えを求め続ける旅路だった。
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
コンパイルして使う場合。
パラメータからひとつを選択して表示する。
; A_Argsはコマンドラインパラメータが格納される配列オブジェクト
MsgBox, , %A_scriptname%, % ListViewSelection(A_Args, "キーを選択(Escでキャンセル)")
例2
めっちゃ深い階層のファイルパスから、ディレクトリ名を抜き出してコピーする。
ようやく冒頭の画像のやつ
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(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に繋がったわけ。
というわけで、選択した全ての項目の値を配列で返すバージョン。
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(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列化で数字アクセラレーターキーによる選択に対応
1列目に数字とかを入れておけば、該当するキーの入力で選択できてなおよい。
ともなって表示を色々調整。
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
}