🐶

PowerShellをタスクトレイに常駐したい

2025/01/02に公開

定期的な処理をスクリプトで手早くでっち上げたいなぁというお話
...いや、タスクスケジューラだと管理権限考えるの面倒くさいんですわ
※あとScheduledTaskってPowerShell7では使えなかったよーな気がする

いやまぁ内容的には定期処理ってトコ以外ほぼ以下の記事の二番煎じなんですが
なんか色々魔改造を追加してそこはかとなく別物チックにはなってますええホント
https://aquasoftware.net/blog/?p=1244
アイコンデータをBase64にしてコード化しつつカラーテーブル弄るのはイカしたアイデアっすね

本題

20250117いくつかこっそり修正

function RunInTaskTray {
    param (
        [Parameter(Mandatory = $true)]  [string]      $Name,
        [Parameter(Mandatory = $true)]  [uint32]      $Color,
        [Parameter(Mandatory = $true)]  [scriptblock] $Conf,
        [Parameter(Mandatory = $true)]  [scriptblock] $Exec,
        [Parameter(Mandatory = $true)]  [int]         $Interval,
        [Parameter(Mandatory = $false)] [string]      $MenuNameExit = "終了",
        [Parameter(Mandatory = $false)] [string]      $MenuNameConf = "設定",
        [Parameter(Mandatory = $false)] [string]      $MenuNameExec = "実行"
    )
    begin {}
    process {
        $mname = "$($Name)Launcher@$Interval)"
        $mutex = New-Object System.Threading.Mutex($false, $mname)
        try {
            # 多重起動回避
            if ($mutex.WaitOne(0, $false)) {
                try {
                    # コンテキスト作成
                    $AppCtxt = New-Object System.Windows.Forms.ApplicationContext

                    # タスクトレイアイコン作成
                    $TrayIcon = [System.Windows.Forms.NotifyIcon]@{
                        Icon            = GenTaskTrayIcon($Color)
                        Text            = $Name
                        BalloonTipIcon  = 'Error'
                        BalloonTipTitle = 'Error'
                    }
                    $TrayIcon.ContextMenuStrip = New-Object System.Windows.Forms.ContextMenuStrip

                    # 設定メニュー
                    if ($MenuNameConf) {
                        $ConfMenu = [System.Windows.Forms.ToolStripMenuItem]@{ Text = $MenuNameConf }
                        $ConfMenu.add_Click({
                            try {
                                $null = $Conf.Invoke()
                            } catch {
                                $TrayIcon.BalloonTipIcon = "Error"
                                $TrayIcon.BalloonTipText = $_.ToString()
                                $TrayIcon.ShowBalloonTip(5000)
                            }
                        })
                        $TrayIcon.ContextMenuStrip.Items.Add($ConfMenu) > $null
                    }

                    # 実行メニュー
                    if ($MenuNameExec) {
                        $ExecMenu = [System.Windows.Forms.ToolStripMenuItem]@{ Text = $MenuNameExec }
                        $ExecMenu.add_Click({
                            $rsl = ""
                            try {
                                $rsl = $Exec.Invoke()
                            } catch {
                                $TrayIcon.BalloonTipIcon = "Error"
                                $TrayIcon.BalloonTipText = $_.ToString()
                                $TrayIcon.ShowBalloonTip(5000)
                            }
                            if ($rsl -ne "") {
                                $TrayIcon.BalloonTipIcon = "Info"
                                $TrayIcon.BalloonTipText = $rsl
                                $TrayIcon.ShowBalloonTip(5000)
                            }
                        })
                        $TrayIcon.ContextMenuStrip.Items.Add($ExecMenu) > $null
                    }

                    # 終了メニュー
                    if ("" -eq $ExitMenu -or $null -eq $ExitMenu) {
                        $ExitMenu = "Exit"
                    }
                    $ExitMenu = [System.Windows.Forms.ToolStripMenuItem]@{ Text = $MenuNameExit }
                    $ExitMenu.add_Click({
                        $AppCtxt.ExitThread()
                    })
                    $TrayIcon.ContextMenuStrip.Items.Add($ExitMenu) > $null

                    # インターバル
                    $TrayTimer = New-Object Windows.Forms.Timer
                    if ($Interval -gt 0){
                        $TrayTimer.Add_Tick({
                            $TrayTimer.Stop()
                            $rsl = ""
                            try {
                                $rsl = $Exec.Invoke()
                            } catch {
                                $TrayIcon.BalloonTipIcon = "Error"
                                $TrayIcon.BalloonTipText = $_.ToString()
                                $TrayIcon.ShowBalloonTip(5000)
                            }
                            if ($rsl -ne "") {
                                $TrayIcon.BalloonTipIcon = "Info"
                                $TrayIcon.BalloonTipText = $rsl
                                $TrayIcon.ShowBalloonTip(5000)
                            }
                            $TrayTimer.Interval = $Interval
                            $TrayTimer.Start()
                        })
                        $TrayTimer.Interval = 5000 # 固定
                        $TrayTimer.Enabled = $true
                        $TrayTimer.Start()
                    }

                    # タスクトレイアイコン登録
                    $TrayIcon.Visible = $true
                    [System.Windows.Forms.Application]::Run($AppCtxt) > $null
                    $TrayIcon.Visible = $false
                    $TrayTimer.Stop()
                } finally {
                    if($TrayTimer){$TrayTimer.Dispose()}
                    if($TrayIcon ){$TrayIcon.Dispose()}
                    if($mutex    ){$mutex.ReleaseMutex()}
                }
            }
        } finally {
            $mutex.Dispose()
        }
    }
    end {}
}

function local:GenTaskTrayIcon([uint32] $ARGB) {
    # PowerShell(ぽい)アイコン画像バイナリ
    # ・16x16 1bit/pixelインデックスカラー画像
    # ・パレット色を書き換えてアイコン背景色を一括変更する
    $icon = 'AAABAAEAEBAQAAEABAB4AAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAEDAAAAJT1tIgAAAAZQTFRFAAB/////8DxOgwAAAC1JREFUCNdjYGBgYGFg4GNgYGdgYG5gYGxgYHgARcwHQIJyDAwWNQwGNQxgAACDjAYG7YuK+QAAAABJRU5ErkJggg=='
    $strm = New-Object System.IO.MemoryStream(,[System.Convert]::FromBase64String($icon))
    $strm.Seek(0x3f, [System.IO.SeekOrigin]::Begin) > $null
    $ARGB = $ARGB -band 0x00ffffff
    $strm.WriteByte($ARGB -shr 16 -band 0xff)
    $strm.WriteByte($ARGB -shr  8 -band 0xff)
    $strm.WriteByte($ARGB -shr  0 -band 0xff)
    $strm.Seek(0x0, [System.IO.SeekOrigin]::Begin) > $null
    return New-Object System.Drawing.Icon($strm)
}

使い方

設定編集と所望処理用のスクリプトブロックを与えて起動します
これで概ね1000ms毎に定期的に処理を実行するハズ

RunInTaskTray "Title" 0x0000ff { Edit } { Exec } 1000

蛇足

実は他の記事についてもまとめるとほぼほぼテンプレコードが出来上がったりします
とりあえずこんだけあれば12時にバックアップ起動とかも簡単に作れるし
やればAutoHotKey的にショートカットキー起動とかもできるよーな気はする

class Conf {
    [string]$aaa
    [string]$bbb
}
# 設定初期化
function local:InitConfFile([string] $Path) {
    if ((Test-Path -LiteralPath $Path) -eq $false) {
        $Conf = New-Object Conf -Property @{
            aaa = "aaa"
            bbb = "bbb"
        }
        SaveConfFile $Path $Conf
    }
}
# 設定書込
function local:SaveConfFile([string] $Path, [Conf] $Conf) {
    $null = New-Item ([System.IO.Path]::GetDirectoryName($Path)) -ItemType Directory -ErrorAction SilentlyContinue
    $Conf | ConvertTo-Json | Out-File -FilePath $Path
}
# 設定読出
function local:LoadConfFile([string] $Path) {
    $json = Get-Content -Path $Path | ConvertFrom-Json
    $Conf = ConvertFromPSCO ([Conf]) $json
    return $Conf
}
# 設定編集
function local:EditConfFile([string] $Title, [string] $Path) {
    $Conf = LoadConfFile $Path
    $ret = ShowSettingDialog $Title $Conf
    if ($ret -eq "OK") {
        SaveConfFile $Path $Conf
    }
}
# 設定初期化
InitConfFile $ConfPath
# 処理実行
RunInTaskTray $Title 0x0000ff { EditConfFile $Title $ConfPath } { Exec } 1000

まず設定ロード時のConvertFromPSCOについては以下
https://zenn.dev/notstrings/articles/77a87cc56f0979

設定画面自体(ShowSettingDialog)については以下
https://zenn.dev/notstrings/articles/4678cc90e79991

設定画面で使うプロパティグリッドの参照する属性については以下
https://zenn.dev/notstrings/articles/b524f7467b8b7f

実はGithubにゴチャゴチャ作ってる最中なんだけど
まだ結構試行錯誤してるんで情報が断片的なのは残念無念

Discussion