2020-06-23に更新

タスクトレイで指令を待ち続ける健気な PowerShell スクリプト

この記事は PowerShell Advent Calendar 2019 の 23日目 の記事だ。
日程が埋まりきっていなかったので、適当な内容で埋めていこう。

あなたは、 PowerShell の短いコードを、 デスクトップのアイコンをクリックするのも億劫なほど、手軽に実行したいと思ったことはないだろうか?

ないって?
私はある。

例えば、ブラウザなどでコピーして WYSIWYG エディターにペタリと貼り付けるときに、クリップボード内の装飾情報を削除したいとか。

(Get-Clipboard -TextFormatType UnicodeText -Raw) -replace '^[ \r\n\t]*|[ \r\n\t]*$','' | Set-Clipboard

PowerShell なら上記のような一行コード実行すれば済む話だ。 しかし、 ウィンドウをシェルに切り替えたり、 ショートカットクリックするのも面倒。 タスクトレイのアイコン一つクリックして実行できたらいいのに。

はい、それを叶えます。

ベースのコード

https://gist.github.com/advanceboy/674407fc5bb23ddfc51b73e670b501c8

<###############################################################
タスクトレイに常駐し、クリックされると特定のスクリプトを実行するコード
###############################################################>
#Requires -Version 3
param(
    # アイコンクリック時に実行する スクリプトファイル または スクリプトブロック
    [parameter(Mandatory)]
    [ValidateNotNull()]
    [ValidateScript({ $_ -is [string] -or $_ -is [scriptblock] })]
    $Script,

    # アイコンの色。初期値は 0x0000007F (#00007F)。
    # 同じ色を指定した場合、同時に起動できなくなる。
    # ARGB<31-24> (不透明度) は無視される
    # ARGB<23-16> は R
    # ARGB<15-8> は G
    # ARGB<7-0> は B
    [ValidateNotNull()]
    [uint32]$ARGB = 0x0000007F,

    # アイコンのツールチップテキスト
    [string]
    $ToolHintText = $null
);
Add-Type -AssemblyName System.Windows.Forms;

# PowerShellっぽい アイコン画像バイナリ。
# 0x3f から 3バイト (PNG の PLTE チャンク) に、 RGB の順番で入っている背景色の色情報を書き換える。
$mems = New-Object System.IO.MemoryStream(,[System.Convert]::FromBase64String('AAABAAEAEBAQAAEABAB4AAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAEDAAAAJT1tIgAAAAZQTFRFAAB/////8DxOgwAAAC1JREFUCNdjYGBgYGFg4GNgYGdgYG5gYGxgYHgARcwHQIJyDAwWNQwGNQxgAACDjAYG7YuK+QAAAABJRU5ErkJggg=='));
$mems.Seek(0x3f, [System.IO.SeekOrigin]::Begin) > $null;
$ARGB = $ARGB -band 0x00ffffff;
$mems.WriteByte($ARGB -shr 16 -band 0xff);
$mems.WriteByte($ARGB -shr 8 -band 0xff);
$mems.WriteByte($ARGB -band 0xff);
$mems.Seek(0x0, [System.IO.SeekOrigin]::Begin) > $null;
$icon = New-Object System.Drawing.Icon($mems);

$MUTEX_NAME = '0b72e703-1de1-4320-ae81-d7c48257e460: ' + [System.BitConverter]::ToString([System.BitConverter]::GetBytes($ARGB));
$mutex = New-Object System.Threading.Mutex($false, $MUTEX_NAME);
try {
    # 多重起動チェック
    if ($mutex.WaitOne(0, $false)) {
        try {
            # コンテキスト作成
            $appContext = New-Object System.Windows.Forms.ApplicationContext;

            # 通知アイコン作成
            $notifyIcon = [System.Windows.Forms.NotifyIcon]@{
                Icon = $icon;
                Text = $ToolHintText;
                BalloonTipIcon = 'Error';
                BalloonTipTitle = 'Error';
            };

            # アイコン左クリック時
            $notifyIcon.add_Click({
                if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
                    try {
                        &$Script;
                    } catch {
                        $notifyIcon.BalloonTipText = $_.ToString();
                        $notifyIcon.ShowBalloonTip(5000);
                    }
                }
            });

            # Exit メニュー
            $menuItem = [System.Windows.Forms.ToolStripMenuItem]@{ Text = 'Exit' };
            $notifyIcon.ContextMenuStrip = New-Object System.Windows.Forms.ContextMenuStrip;
            [void]$notifyIcon.ContextMenuStrip.Items.Add($menuItem);
            $menuItem.add_Click({
                $appContext.ExitThread();
            });

            # 表示
            $notifyIcon.Visible = $true;
            [void][System.Windows.Forms.Application]::Run($appContext);
            $notifyIcon.Visible = $false;
        } finally {
            $notifyIcon.Dispose();
            $mutex.ReleaseMutex();
        }
        $retcode = 0;
    } else {
        $retcode = 255;
    }
} finally {
    $mutex.Dispose();
}
exit $retcode;

これを、スタートアップフォルダのバッチファイルなどで、こんな感じで実行する。

191223_Register-TaskTrayScripts-00.png

start "" powershell -WindowStyle Hidden .\Register-TaskTrayScripts.ps1 -Script "{ (Get-Clipboard -Raw) -replace '^[ \r\n\t]*|[ \r\n\t]*$','' | Set-Clipboard }" -ARGB 0x008080

すると、タスクトレイにアイコンが現れる。

こいつをクリックすれば、 -Script パラメータで指定したコードが実行されるという算段だ。

複数のスクリプトを使い分けたければ、 -ARGB パラメータでアイコンの色を変えて複数のスクリプトを指定すれば OK だ。

常駐を修了させたいときは、 タスクトレイのアイコンを右クリックして Exit でどうぞ。

ちょっと解説

基本的には、 System.Windows.Forms で登録した通知アイコンがクリックされたら、 スクリプトブロックを実行しているだけだ。

適当に作った極小 .ico ファイルを、 Base64 でソースコードに埋め込むことで、通知アイコンの画像を実現している。
この .ico ファイルは、インデックスカラーの PNG を抱えており、 そのインデックスの色をバイナリ上で書き換えることで、アイコンの色変更を実現している。
泥臭いね。

その色を指定している ARGB パラメータの内容で、 多重起動制御用のミューテックスを作成している。
このため、同じ色のアイコンを登録しようとすると、通知アイコンを登録せずに終了するようになっている。
この仕組みのせいで、登録されるアイコンの最大数はわずか 16777216個 のみとなっている。

改訂履歴

  • 2019-12-23: PowerShell 7 RC で動かない問題を修正。
Originally published at aquasoftware.net
ツイッターでシェア
みんなに共有、忘れないようにメモ

advanceboy

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント