はじめに
PowerShellをバックグラウンドで動かしているとWrite-Hostなどの表示コマンドで出力したメッセージが確認できなくて、必要な情報を見逃すことがありました。
Write-Host以外にも標準で提供されているWrite-Progressコマンドが結構便利なのでよく使うのですが、見えなければ意味がないので、ユーザー通知の方法を見直してみます。
PowerShellからトーストを制御することでデスクトップにメッセージや進捗率を表示する方法を調べてみました。
トースト表示
最近のWindows(8以降かな?)ではトースト(Toast)表示の機能が提供されています。Androidだとトーストを通知に使うのが一般的ですのでWindowsをタブレットとして利用ために必要だったのでしょう。
Windowsのトーストはデスクトップの右下に表示されます。タスクトレイから表示するバルーンと表示が近いのですが、画像表示、ボタン、コンテキストメニュー、プログレスバーなどの表示が可能です。
バルーンはタスクトレイに常駐してから表示する必要がありますが、トーストはそのまま表示できる仕様になっていて情報を通知する機能に特化しています。
ボタンやコンテキストメニューはPowerShellから制御するには向かないようなので、文字列のみのトーストと、画像をつけたトースト、プログレスバーで進捗率を表示するトーストの表示を確認します。
テキストのみのトースト表示
最初はシンプルにテキストのみのトースト表示を行います。
1~3行で表示できる仕様なので、3行をそれぞれ指定するコマンドとして実装してみました。
Function ShowToast {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)][String] $title,
        [Parameter(Mandatory=$true)][String] $message,
        [Parameter(Mandatory=$true)][String] $detail
    )
    [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
    $app_id = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
    $content = @"
<?xml version="1.0" encoding="utf-8"?>
<toast>
    <visual>
        <binding template="ToastGeneric">
            <text>$($title)</text>
            <text>$($message)</text>
            <text>$($detail)</text>
        </binding>
    </visual>
</toast>
"@
    $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
    $xml.LoadXml($content)
    $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
    [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app_id).Show($toast)
}
xmlの中のtextタグで表示する文字列を指定しています。
$($title)のように記載すると$title変数の文字列が表示される仕様になっています。
実行結果
ShowToast -title title_string -message message_string -detail detail_string
画像をつけてトースト表示
トーストにはテキストの左に表示するアイコン画像、テキストの上に表示するヒーロー画像、テキストの下に表示するインライン画像を指定できます。
タスクトレイのバルーン表示だとメッセージ用にアイコンくらいしか表示できなかったので、ちょっと便利になっています。
以下のサンプルは引数でアイコン画像とヒーロー画像を指定できるように実装してみました。
ファイルは相対パスだと動作しないので、絶対パスで指定してください。
Function ShowImageToast {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)][String] $title,
        [Parameter(Mandatory=$true)][String] $message,
        [Parameter(Mandatory=$true)][String] $detail,
        [Parameter(Mandatory=$true)][String] $icon_path,
        [Parameter(Mandatory=$true)][String] $hero_path
    )
    [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
    $app_id = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
    $content = @"
<?xml version="1.0" encoding="utf-8"?>
<toast>
    <visual>
        <binding template="ToastGeneric">
            <text>$($title)</text>
            <text>$($message)</text>
            <text>$($detail)</text>
            <image placement="appLogoOverride" hint-crop="circle" src="$($icon_path)"/>
            <image placement="hero" src="$($hero_path)"/>
        </binding>
    </visual>
</toast>
"@
    $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
    $xml.LoadXml($content)
    $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
    [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app_id).Show($toast)
}
src属性に$($icon_path)を指定することでパスの情報を渡しています。
実行結果
ShowImageToast -title title_string -message message_string -detail detail_string -icon_path "C:/icon.png" -hero_path "C:/hero.png"
進捗率をつけてトースト表示
プログレスバー付のトーストを制御するには表示と更新を別に処理する必要があります。
サンプルは2つに分けて実装しました。
Function ShowProgressToast {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)][String] $tag,
        [Parameter(Mandatory=$true)][String] $group,
        [Parameter(Mandatory=$true)][String] $title,
        [Parameter(Mandatory=$true)][String] $message,
        [Parameter(Mandatory=$true)][String] $progressTitle
    )
    [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
    $app_id = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
    $content = @"
<?xml version="1.0" encoding="utf-8"?>
<toast>
    <visual>
        <binding template="ToastGeneric">
            <text>$($title)</text>
            <text>$($message)</text>
            <progress value="{progressValue}" title="{progressTitle}" valueStringOverride="{progressValueString}" status="{progressStatus}" />
        </binding>
    </visual>
</toast>
"@
    $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
    $xml.LoadXml($content)
    $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
    $toast.Tag = $tag
    $toast.Group = $group
    $toast_data = New-Object 'system.collections.generic.dictionary[string,string]'
    $toast_data.add("progressTitle", $progressTitle)
    $toast_data.add("progressValue", "")
    $toast_data.add("progressValueString", "")
    $toast_data.add("progressStatus", "")
    $toast.Data = [Windows.UI.Notifications.NotificationData]::new($toast_data)
    $toast.Data.SequenceNumber = 1
    [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app_id).Show($toast)
}
Function UpdateProgessToast {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)][String] $tag,
        [Parameter(Mandatory=$true)][String] $group,
        [Parameter(Mandatory=$true)][String] $value,
        [Parameter(Mandatory=$true)][String] $message,
        [Parameter(Mandatory=$true)][String] $status
    )
    $app_id = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
    $toast_data = New-Object 'system.collections.generic.dictionary[string,string]'
    $toast_data.add("progressValue", $value)
    $toast_data.add("progressValueString", $message)
    $toast_data.add("progressStatus", $status)
    $progress_data = [Windows.UI.Notifications.NotificationData]::new($toast_data)
    $progress_data.SequenceNumber = 2
    [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app_id).Update($progress_data, $tag , $group) | Out-Null
}
表示時のXMLのprogressタグにvalue="{progressValue}"というような記述を入れておき、
更新時のUpdate()に渡すNotificationDataのdictionaryにprogressValueをキーとして0~1の値を文字列として設定するとプログレスバーが更新できます。
表示と更新を同じトーストにおこなうために、tagとgroupの文字列を指定します。これが一致していないと更新されません。
公式の説明がちょっとわかりにくくてハマりました。
結局、PowerShellだけでは問題解決できなくてC#で同じ動作するアプリを書いてパラメータ比較することで更新方法が判明しました。
最初はサンプルをWrite-Progressに合わせた仕様で実装しようと思ったけど、うまく動作する仕様にできない気がしたのでやめておきます。
実行結果
$tag_name = "my_tag"
$group_name = "my_group"
$max_count = 5
# 表示
ShowProgressToast -tag $tag_name -group $group_name -title title_string -message message_string -progressTitle progress_title
# 更新するループ
for($index=0; $index -le $max_count; $index++) {
    $value = $index / $max_count
    $message = "[" + $index + "/" + $max_count + "]"
    UpdateProgessToast -tag $tag_name -group $group_name -value $value -message $message -status progress_status
    Start-Sleep -Seconds 1
}
機能の組み合わせ
テキスト、イメージ、プログレスバーの表示を組み合わせることもできるので、必要に応じてxmlを編集してください。
ここで紹介した以外の機能は公式のドキュメントを参照してください。
その他tips
日本語を表示すると文字化けする場合は場合はUTF-8 With BOMで保存していないと思います。ps1ファイル作成時の文字コード指定にご注意ください。
おわりに
こちらの記事でAdvent Calendarに参加してみたのですが、技術的なことをあまり紹介できなくてちょっと不完全燃焼だったので、PowerShellで技術ネタを書いてみました。
いつもは技術ネタの作成が先で暇な時に記事を書いているのですが、今回は記事を作ろうと思ってからネタを考えました。調査よりネタ探しの方が疲れますね。
思い付きで書き始めましたが、実用的なネタになったと思います。
トーストにはボタンやコンテキストメニューも表示できますが、PowerShellからできることが少なくてあまり有効な使い道が思いつかないので今回は調査していません。デスクトップからは使いにくい仕様なんですよね。
私PowerShellだけど…シリーズ
私PowerShellだけど、君のタスクトレイで暮らしたい
私PowerShellだけど「送る」からファイルを受け取りたい(コンテキストメニュー登録もあるよ)
私powershellだけどタスクトレイの片隅でアイを叫ぶ
私PowerShellだけど子を持つ親になるのはいろいろ大変そう
私Powershellだけど日付とサイズでログを切り替えたい(log4net)
私PowerShellだけどスクリプトだけでサービス登録したい
私PowerShellだけどスクリーンショットを撮影したい
私PowerShellだけどマウスカーソル付きのスクリーンショットを撮影したい
私PowerShellだけどスリープ抑制したい



