UWSCでタスクトレイ常駐する

公式掲示板にUWSCでタスクトレイ常駐する方法はないか、という質問があった。
需要があるようなので、考えてみた。
TEXTBLOCKの部分はPowershellなので、PowershellでNotifyIconを作成するサンプルでもあります。

追記 2014/11/21
高機能版に、親プロセス(=UWSC)がいなくなったら終了する処理をつけた。
これにより、UWSCを停止ボタンで止めてしまっても、UWSCを終了すればアイコンは消える。

スクリプト

notifyIcon.uws

OPTION EXPLICIT

IFB GET_UWSC_NAME = "notifyIcon.uws" THEN
	MSGBOX("ESCキーでタスクトレイ常駐終了します")
	DIM id = NotifyIcon.Start("NotifyIcon.SampleCallBack(p)", "iconApp.ico")
	WHILE !GETKEYSTATE(VK_ESC)
		SLEEP(1)
	WEND
	CTRLWIN(id, CLOSE)
ENDIF



MODULE NotifyIcon

	TEXTBLOCK _script
		Add-Type -AssemblyName System.Windows.Forms
		$formMain = New-Object System.Windows.Forms.Form
		$formMain.Opacity = 0
		$formMain.Text = '$$formName$$'
		$textBox = New-Object System.Windows.Forms.TextBox
		$formMain.Controls.Add($textBox)
		$components = New-Object System.ComponentModel.Container
		$notifyIcon = New-Object System.Windows.Forms.NotifyIcon($components)
		$notifyIcon.Icon = New-Object System.Drawing.Icon('$$iconPath$$')
		$notifyIcon.Text = "$$tooltip$$"
		$notifyIcon.Add_MouseClick({
		 Param (
		  $sender,
		  $event
		 )
		 $textBox.Text = $event.Button
		})
		$notifyIcon.Add_MouseDoubleClick({
		 Param (
		  $sender,
		  $event
		 )
		 $textBox.Text = 'D' + $event.Button
		})
		$notifyIcon.Visible = $true
		$formMain.ShowDialog()
		$components.Dispose()
		$formMain.Dispose()
	ENDTEXTBLOCK

	FUNCTION Start(callback, iconPath, tooltip=EMPTY, formName=EMPTY, wait=-1)
		IF tooltip=EMPTY THEN tooltip = GET_UWSC_NAME
		IF formName=EMPTY THEN formName = "NotifyIcon form " + GET_UWSC_NAME

		POWERSHELL(REPLACE(REPLACE(REPLACE(_script, "$$formName$$", formName), "$$iconPath$$", iconPath), "$$tooltip$$", tooltip), TRUE)
		RESULT = GETID(formName, , wait)
		THREAD _Watch(RESULT, formName, callback)
	FEND

	PROCEDURE _Watch(id, formName, callback)
		DIM p
		WHILE STATUS(id, ST_TITLE) = formName
			p = GETSTR(id)
			IFB LENGTH(p) THEN
				EVAL(callback)
				SENDSTR(id, "", 1, TRUE)
			ELSE
				SLEEP(0.1)
			ENDIF
		WEND
	FEND


	PROCEDURE SampleCallBack(p)
		MSGBOX(p)
	FEND

ENDMODULE

注意

  • UWSCを停止ボタン等で終了すると、NotifyIcon用Powershellプロセスが残ったままとなり、アイコンが消えません
    • 非表示フォームを閉じれば、アイコンは消えPowershellプロセスも終了します
    • 初期状態のフォームなら「CTRLWIN(GETID("NotifyIcon form "), CLOSE)」を実行してください
    • Powershellのプロセスをタスクマネージャー等で終了しても良いですが、次の注意にひっかかります
  • NotifyIcon用Powershellプロセスをタスクマネージャー等から強引に終了してもアイコンが残ります

解説他

使い方は簡単、、、かな?
NotifyIcon.Start関数を呼ぶだけ。
リターンがIDなので、終わったらCTRLWINでCLOSEしてやってください。


NotifyIcon.Start関数のパラメーターは、

  1. コールバック関数。EVALで実行する。
  2. アイコンのパス
  3. ツールチップ。省略時はスクリプト
  4. タスクトレイアイコン用の隠しフォームの名前。省略可
  5. 隠しフォーム作成待ち時間。省略時は-1(無限待ち)


隠しフォームの名前は通常指定する必要はありませんが、複数の通知アイコンを作成したい場合は、指定してください。
タスクトレイアイコンを、クリックしたりダブルクリックしたりすると、コールバック関数が呼ばれます。
コールバック関数は、_Wait関数を実行するスレッドから呼ばれます。
サンプルだと、引数の値をMSGBOXで表示するので、サンプル実行して動作を見てください。
一応、

  • 左クリックで、「Left」
  • 右クリックで、「Right」
  • 左ダブルクリックで、「DLeft」
  • 右ダブルクリックで、「DRight」

が入ってくるはずです。


公式掲示板では、Win32APIを呼ぶ方向で話が進んでましたが、それは面倒です。
Powershell使いましょうよ。とても優秀ですから。

高機能版

上のはシンプルなので残していますが、ついでに高機能版を作成しました。
上のとの違いは、

  • アイコンは、関連付けからの抽出に対応した
  • アイコンのパスを省略すると、自スクリプトから取り出すようにした
  • アイコンをBase64文字列でも指定可能にした(アイコンファイルをBase64文字列にしたもの)
  • 不明なものを指定された場合、(スクリプト内に書かれた)適当なアイコンを表示する
  • アイコンを後から変更可能にした
  • ツールチップも後から変更可能にした
  • SampleCallback関数を右クリックメニューに対応した
  • 親プロセスが終了したら終了する


notifyIcon.uws

OPTION EXPLICIT


IFB GET_UWSC_NAME = "notifyIcon.uws" THEN
	MSGBOX("右クリックメニューでタスクトレイ常駐終了できます")
	DIM id = NotifyIcon.Start(), id2=NotifyIcon.Start(EMPTY, "c:\windows\system32\calc.exe", "電卓ではありません", "NotifyIcon form second")
	WHILE STATUS(id, ST_ISID) OR STATUS(id2, ST_ISID)
		SLEEP(1)
	WEND
ENDIF



MODULE NotifyIcon

	TEXTBLOCK _script
		Add-Type -AssemblyName System.Windows.Forms
		$formMain = New-Object System.Windows.Forms.Form
		$formMain.Opacity = 0
		$formMain.Text = '$$formName$$'
		$textBox = New-Object System.Windows.Forms.TextBox
		$formMain.Controls.Add($textBox)
		$components = New-Object System.ComponentModel.Container
		$notifyIcon = New-Object System.Windows.Forms.NotifyIcon($components)
		$notifyIcon.Add_MouseClick({
			Param ($sender, $event)
			$textBox.Text = $event.Button
		})
		$notifyIcon.Add_MouseDoubleClick({
			Param ($sender, $event)
			$textBox.Text = 'D' + $event.Button
		})
		$textBoxTooltip = New-Object System.Windows.Forms.TextBox
		$textBoxTooltip.Add_TextChanged({
			$notifyIcon.Text = $textBoxTooltip.Text
		})
		$textBoxTooltip.Location = New-Object System.Drawing.Point(0, $textBox.Bottom)
		$formMain.Controls.Add($textBoxTooltip)
		$textBoxTooltip.Text = "$$tooltip$$"
		$textBoxIcon = New-Object System.Windows.Forms.TextBox
		$textBoxIcon.Add_TextChanged({
			try {
				$icon = New-Object System.Drawing.Icon($textBoxIcon.Text)
			} catch {
				try {
					$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($textBoxIcon.Text)
				} catch {
					$icon = $null
				}
			}
			if($icon -eq $null) {
				try {
					$icon = New-Object System.Drawing.Icon(New-Object System.IO.MemoryStream([Convert]::FromBase64String($textBoxIcon.Text), $false))
				} catch {
					$icon = New-Object System.Drawing.Icon(New-Object System.IO.MemoryStream([Convert]::FromBase64String(@'
						AAABAAEAEBAQAAAABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAABA
						AAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP
						//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
						AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
						AAAAAAAAAAAAAAAAAAAAAAD//wAA888AAO23AAD9vwAA878AAO+/AADttwAA888AAP//AADn1wAA29c
						AANvXAADbqwAA26sAANurAAD//wAA
'@						), $false))
				}
			}
			if($icon -ne $null) {
				if($notifyIcon.Icon -ne $null) {
					$notifyIcon.Visible = $false
					$notifyIcon.Icon.Dispose()
				}
				$notifyIcon.Icon = $icon
				$notifyIcon.Visible = $true
			}
		})
		$textBoxIcon.Location = New-Object System.Drawing.Point(0, $textBoxTooltip.Bottom)
		$formMain.Controls.Add($textBoxIcon)
		$textBoxIcon.Text = '$$iconData$$'
		$textBoxPpid = New-Object System.Windows.Forms.TextBox
		$textBoxPpid.Location = New-Object System.Drawing.Point(0, $textBoxIcon.Bottom)
		$pp = $null
		$textBoxPpid.Add_TextChanged({
			if($pp -ne $null) { $pp.Dispose() }
			$pp = [System.Diagnostics.Process]::GetProcessById($textBoxPpid.Text)
			$pp.Add_Exited({ $formMain.Close() })
			$pp.SynchronizingObject = $formMain
			$pp.EnableRaisingEvents = $true
		})
		$formMain.Controls.Add($textBoxPpid)
		$formMain.ShowDialog()
		if($pp -ne $null) { $pp.Dispose() }
		$components.Dispose()
		$formMain.Dispose()
	ENDTEXTBLOCK

	PROCEDURE _Watch(id, callback)
		DIM p
		WHILE STATUS(id, ST_ISID)
			p = GETSTR(id, 1)
			IFB LENGTH(p) THEN
				SENDSTR(id, "", 1, TRUE)
				EVAL(callback)
			ELSE
				SLEEP(0.1)
			ENDIF
		WEND
	FEND


	FUNCTION Start(callback=EMPTY, iconData=EMPTY, tooltip=EMPTY, formName=EMPTY, wait=-1)
		IF callback=EMPTY THEN callback = "SampleCallBack(id, p)"
		IF iconData=EMPTY THEN iconData = GET_UWSC_NAME
		IF tooltip=EMPTY THEN tooltip = GET_UWSC_NAME
		IF formName=EMPTY THEN formName = "NotifyIcon form " + GET_UWSC_NAME

		POWERSHELL(REPLACE(REPLACE(REPLACE(_script, "$$formName$$", formName), "$$iconData$$", iconData), "$$tooltip$$", tooltip), TRUE)
		RESULT = GETID(formName, , wait)
		SENDSTR(RESULT, STATUS(GETID(GET_THISUWSC_WIN), ST_PROCESS), 4, TRUE)
		THREAD _Watch(RESULT, callback)
	FEND

	PROCEDURE ChangeTooltip(id, tip)
		SENDSTR(id, tip, 2, TRUE)
	FEND

	FUNCTION GetTooltip(id)
		RESULT = GETSTR(id, 2)
	FEND

	PROCEDURE ChangeIcon(id, icon)
		SENDSTR(id, icon, 3, TRUE)
	FEND

	FUNCTION GetIcon(id)
		RESULT = GETSTR(id, 3)
	FEND


	PROCEDURE SampleCallBack(id, p)
		SELECT p
		CASE "Right"
			SELECT POPUPMENU(SPLIT("ChangeIcon ChangeTooltip Exit"))
			CASE 0
				ChangeIcon(id, INPUT("icon?", GetIcon(id)))
			CASE 1
				ChangeTooltip(id, INPUT("tooltip?", GetTooltip(id)))
			CASE 2
				CTRLWIN(id, CLOSE)
			SELEND
		DEFAULT
			MSGBOX(p)
		SELEND
	FEND

ENDMODULE

何故か、Process.Exitedイベントを使うとPowershellが動作を停止するという、、、。
しかたないので、タイマー監視。

追記 2014/11/28
Process.Exitedイベントの使い方がわかったので修正