PowerShellでポップアップメニューを表示する

PowerShellでポップアップメニューを表示してみたくなった。
ContextMenuStripを使うべきっぽいんだけど、横着してContextMenuで。
一度でも呼ぶと、System.Windows.Formsがロードされるのが不可逆。



追記 20150707
フォント変更可能なバージョンを作成しました。
http://d.hatena.ne.jp/junjun777/20150707/powershell_popupmenu

スクリプト

function Show-PopupMenu() {
	Param (
		[string[]]$Items,
		[string]$Splitter = ',',
		[string]$Separator = '^',
		[int]$x = [int]::MinValue,
		[int]$y = [int]::MinValue
	)

	if(!('System.Windows.Forms.Form' -as [type])) {
		Add-Type -AssemblyName System.Windows.Forms
	}
	if($Items.Count -eq 1 -and ![string]::IsNullOrEmpty($Splitter)) {
		$Items = $Items[0].Split($Splitter)
	}

	$form = New-Object System.Windows.Forms.Form
	$form.FormBorderStyle = 'None'
	$form.Opacity = 0
	$form.ShowInTaskbar = $false
	$form.StartPosition = 'Manual'
	$onClick = {
		Param ($sender, $ev)
		$form.Name = $sender.Name
	}
	$context = New-Object System.Windows.Forms.ContextMenu
	$mis = New-Object 'System.Collections.Generic.List[System.Collections.Generic.List[System.Windows.Forms.MenuItem]]'
	$mis.Add((New-Object 'System.Collections.Generic.List[System.Windows.Forms.MenuItem]'))
	$idxb = 0
	$idxs = 0
	$idxsp = @()

	for($i = 0; $i -lt $Items.Count; $i++) {
		$put = $true
		if([string]::IsNullOrEmpty($Separator)) {
			$it = @()
			$it += $Items[$i]
		} else {
			$it = $Items[$i].Split($Separator, 2)
		}
		if($it[0].Length -gt 0) {
			if($it[0][0] -eq '\') {
				switch($it[0][1]) {
					'>' {
						$idxsp += $idxs
						$idxb++
						$idxs = 0
						$mis.Add((New-Object 'System.Collections.Generic.List[System.Windows.Forms.MenuItem]'))
						$put = $false
					}
					'<' {
						$idxb--;
						$idxs = $idxsp[$idxb]
						$mis[$idxb][$idxs - 1].MenuItems.AddRange($mis[$idxb + 1].ToArray())
						$put = $false
					}
					default {
						$it[0] = $it[0].Substring(1);
					}
				}
			}
		}
		if($put) {
			$mis[$idxb].Add((New-Object System.Windows.Forms.MenuItem $it[0],$onClick))
			if($it.Count -eq 1) {
				$mis[$idxb][$idxs].Name = $it[0]
			} else {
				$mis[$idxb][$idxs].Name = $it[1]
			}
			$idxs++
		}
	}

	$context.MenuItems.AddRange($mis[0].ToArray())
	if($x -eq [int]::MinValue) { $x = [System.Windows.Forms.Cursor]::Position.X }
	if($y -eq [int]::MinValue) { $y = [System.Windows.Forms.Cursor]::Position.Y }
	$form.Location = New-Object System.Drawing.Point $x,$y
	$timer = New-Object System.Windows.Forms.Timer
	$timer.Interval = 10;
	$timer.Add_Tick({
		$timer.Enabled = $false
		if($form.FormBorderStyle -eq 'None') {
			$context.Show($form, [System.Drawing.Point]::Empty)
			$form.FormBorderStyle = 'Sizable'
			$timer.Enabled = $true
		} elseif($form.FormBorderStyle -eq 'Sizable') {
			$form.Close()
		}
	})
	$timer.Enabled = $true
	[void]$form.ShowDialog()
	$ret = $form.Name
	$context.Dispose()
	$form.Dispose()

	return $ret
}

使い方

基本的な使い方は

Show-PopupMenu 'a,b,c'

といった感じ。
選択すると、選択したものが、キャンセルは空文字。


出す場所を指定する場合は

Show-PopupMenu 'a,b,c' -x 100 -y 100

省略した場合は、ポインター位置です。


階層を構成したい場合は、「\>」と「\<」を使う。

Show-PopupMenu 'a,\>,aa,ab,\>,aba,abb,\<,ac,\<,b,c'


なお、アクセッサーを指定したくて、表示と戻りを変えたい場合は「^」を使う

Show-PopupMenu '&a^a,\>,a&a^aa,a&b^ab,\>,ab&a^aba,ab&b^abb,\<,a&c^ac,\<,&b^b,&c^c'


選択肢に「,」を使いたい場合、分割する文字列を変更する。

Show-PopupMenu 'a/,/c' -Splitter '/'


同様に選択肢に「^」を使いたい場合、分割する文字列を変更する。

Show-PopupMenu "`t(&a)/a,&b^b,&c/c" -Separator '/'


選択肢の先頭に「\」を使いたい場合、「\\」にする。(先頭以外は「\」で良い)

Show-PopupMenu '\\a,\\b\,\\>'


選択肢は配列でも可能

Show-PopupMenu 'a','\>','aa','ab','\>','aba','abb','\<','ac','\<','b','c'

のうがき

タイマー使って、妙なこすいことしてます。
ContextMenuのShowメソッドは、メニューが閉じると戻ってくるのですが、戻って即Disposeしちゃうと、MenuItemのClickイベントが処理されないのです。
このため、Clickイベントを処理する間にメッセージポンプが回るよう、タイマーを使ってFormをモーダルダイアログにしています。


これで何するかって?
タブ補完をもう少し便利にできるんじゃないかなー、と妄想しているとこ。