PowerShellフォームメニュー:スクリプトへのクイックアクセス

週末のプロジェクトの時間です。今日は、最も必要なPowerShellスクリプトを簡単に起動できる軽量なシステムトレイのPowerShellフォームメニューの作り方を学びます。下記に最終結果をご覧いただけます。

Launching PowerShell scripts via a system tray menu icon

この記事では、プロセスを段階的に分解して、独自のPowerShellメニューGUIを作成する方法を学びます。

環境と知識の要件

取り組む前に、以下の最低要件を満たしていることを確認してください:

  • Windows 7以降
  • Windows PowerShell 3以降 – .NET Core 3.0の最新バージョンとPowerShell 7プレビューは、WPFとWinFormのサポートが追加されたため、Windowsで動作する可能性がありますが、テストはされていません。
  • .NET Framework 4.5以降
  • A familiarity with Windows Forms (WinForms)  You can, however, due this with WPF too though.

このプロジェクトでは、主に以下のコンポーネントに依存するため、Visual StudioPoshGUIなどのUI開発ツールに頼る必要はありません:

  • NotifyIcon – ユーザーが操作できるカスタマイズ可能なシステムトレイアイコンです。
  • コンテキストメニュー – ユーザーがトレイアイコンを右クリックしたときのコンテナ。
  • メニューアイテム – 右クリックメニュー内の各オプションの個別のオブジェクト。

お気に入りのPowerShellスクリプトエディタを開いて、始めましょう!

このプロジェクトでは、3つの関数を作成します。コンソールを表示/非表示にするための2つの関数と、システムトレイメニューにアイテムを追加するための1つの関数です。これらの関数は、後でより簡単になるための基盤として機能し、この記事の後半で少し詳しく学ぶことができます。

コンソールウィンドウの表示/非表示

PowerShellスクリプトを起動すると、おなじみのPowerShellコンソールが起動します。作成するPowerShellフォームのメニューアイテムはスクリプトを起動するため、コンソールが起動しないようにする必要があります。単に実行するだけです。

スクリプトが実行されると、少しの.NETを使用してPowerShellコンソールウィンドウを表示するかどうかを切り替えることができます。

現在のセッションにWindow .NETタイプを追加します。これを行うには、以下に示すようにC#を使用します。現在のPowerShellスクリプトのコンテキストにロードする必要がある2つのメソッドはGetConsoleWindowShowWindowです。これらのDLLをメモリに読み込むことで、APIの一部を公開し、PowerShellスクリプトのコンテキストで使用できるようにします。

 #DLLを現在のコンソールセッションのコンテキストに読み込む
 Add-Type -Name Window -Namespace Console -MemberDefinition '
    [DllImport("Kernel32.dll")]
    public static extern IntPtr GetConsoleWindow();
 
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);

次に、次のようにGetConsoleWindow()およびShowWindow()メソッドを使用して、上記で読み込んだDLLを使用する2つの関数を作成します。

 function Start-ShowConsole {
    $PSConsole = [Console.Window]::GetConsoleWindow()
    [Console.Window]::ShowWindow($PSConsole, 5)
 }
 
 function Start-HideConsole {
    $PSConsole = [Console.Window]::GetConsoleWindow()
    [Console.Window]::ShowWindow($PSConsole, 0)
 }

これらの2つの関数を使用することで、コンソールウィンドウを表示または非表示にする方法を作成しました。

注意:メニューを介して実行されるスクリプトの出力を表示したい場合は、PowerShellトランスクリプトまたは他のテキストベースのログ機能を使用することができます。これにより、WindowStyleパラメータを使用してPowerShellセッションを実行するだけではなく、制御を保持することができます。

次に、Start-HideConsoleを呼び出してスクリプトコードの構築を開始します。PowerShellフォームのメニュードリブンスクリプトが実行されると、PowerShellコンソールウィンドウが表示されないようになります。

<#
	関数とオブジェクトの初期化をメモリに読み込む
	テキストベースのローディングバーを表示するか、Write-Progressをホストに表示する
#>
 
Start-HideConsole
 
<#
	フォーム/システムトレイアイコンの表示コード
	これにより、コンソールは閉じるまで待機します
#>

メニューオプションの作成

では、メニューオプションの作成時に新しいオプションを簡単に作成できるように、New-MenuItemという別の関数を作成します。この関数を呼び出すと、MenuItem .NETオブジェクトが作成され、後でメニューに追加できます。

各メニューオプションは別のスクリプトを起動するか、ランチャーを終了するため、New-MenuItem関数には3つのパラメータがあります:

  • Text – ユーザーがクリックするラベル
  • MyScriptPath – 実行するPowerShellスクリプトのパス
  • ExitOnly – ランチャーを終了するオプション

以下の関数スニペットをメニュースクリプトに追加してください。

 function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )

New-MenuItem関数の作成を続けるために、MenuItemオブジェクトを変数に割り当てます。

 #初期化
 $MenuItem = New-Object System.Windows.Forms.MenuItem

次に、メニューアイテムにテキストラベルを割り当てます。

 #希望のテキストを適用
 if($Text) {
 	$MenuItem.Text = $Text
 }

そして、MenuItemにMyScriptPathというカスタムプロパティを追加します。このパスは、メニューでアイテムがクリックされたときに呼び出されます。

 #クリックイベントのロジックを適用する
 if($MyScriptPath -and !$ExitOnly){
 	$MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty

MenuItemにクリックイベントを追加し、所望のスクリプトを起動します。Start-Processは、PowerShellが利用できない場合や提供されたパスにスクリプトが存在しない場合など、スクリプトの起動時に発生するエラーをcatchブロックに受けるためのクリーンな方法を提供します。try/catchブロック内でこれを行ってください。

   $MenuItem.Add_Click({
        try{
            $MyScriptPath = $This.MyScriptPath #クリックイベント中に正しいパスを見つけるために使用されます
            
            if(Test-Path $MyScriptPath){
                Start-Process -FilePath "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList "-NoProfile -NoLogo -ExecutionPolicy Bypass -File `"$MyScriptPath`"" -ErrorAction Stop
            } else {
                throw "Could not find at path: $MyScriptPath"
            }
        } catch {
          $Text = $This.Text
          [System.Windows.Forms.MessageBox]::Show("Failed to launch $Text`n`n$_") > $null
        }
  })

ランチャーに終了条件を提供し、新しく作成したMenuItemを実行時に別の変数に割り当てるための残りのロジックを追加してください。

    #ランチャーを終了する方法を提供します
    if($ExitOnly -and !$MyScriptPath){
        $MenuItem.Add_Click({
            $Form.Close()
    
            #ハングしたプロセスを処理します
            Stop-Process $PID
        })
    }
 
 	 #新しいMenuItemを返します
    $MenuItem
 }

これで、New-MenuItem関数が作成されました!最終的な関数は次のようになります:

 function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )
 
     #初期化
     $MenuItem = New-Object System.Windows.Forms.MenuItem
 
     #所望のテキストを適用する
     if($Text){
         $MenuItem.Text = $Text
     }
 
     #クリックイベントのロジックを適用する
     if($MyScriptPath -and !$ExitOnly){
         $MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty
     }
 
     $MenuItem.Add_Click({
             try{
                 $MyScriptPath = $This.MyScriptPath #クリックイベント中に適切なパスを見つけるために使用する
             
                 if(Test-Path $MyScriptPath){
                     Start-Process -FilePath "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList "-NoProfile -NoLogo -ExecutionPolicy Bypass -File `"$MyScriptPath`"" -ErrorAction Stop
                 } else {
                     throw "Could not find at path: $MyScriptPath"
                 }
             } catch {
                 $Text = $This.Text
                 [System.Windows.Forms.MessageBox]::Show("Failed to launch $Text`n`n$_") > $null
             }
         })
 
     #ランチャーからの終了方法を提供する
     if($ExitOnly -and !$MyScriptPath){
         $MenuItem.Add_Click({
                 $Form.Close()
    
                 #ハングしたプロセスを処理する
                 Stop-Process $PID
             })
     }
 
     #新しいMenuItemを返す
     $MenuItem
 }

New-MenuItem関数をテストするには、上記のコードをPowerShellコンソールにコピーして貼り付け、偽のパラメータ値を指定して関数を実行します。.NETのMenuItemオブジェクトが返されることがわかります。

 PS51> (New-MenuItem -Text "Test" -MyScriptPath "C:\test.ps1").GetType()
 
 IsPublic IsSerial Name                                     BaseType
 -------- -------- ----                                     --------
 True     False    MenuItem                                 System.Windows.Forms.Menu

ランチャーフォームの作成

このようなさらなるヒントをお求めですか?私の個人的なPowerShellブログをチェックしてください:https://nkasco.com/FriendsOfATA

新しいメニューアイテムを簡単に作成できるようになったので、メニューを表示するシステムトレイのランチャーを作成しましょう。

コンポーネントを追加するための基本的なフォームオブジェクトを作成します。これはユーザーに表示されるものではないため、フォームは背後でコンソールを実行し続けます。

 #コンポーネントのコンテナとして機能するフォームを作成する
 $Form = New-Object System.Windows.Forms.Form
 ​
 #フォームを非表示に設定する
 $Form.BackColor = "Magenta" #透明性のためにこの色をTransparencyKeyプロパティにマッチさせる
 $Form.TransparencyKey = "Magenta"
 $Form.ShowInTaskbar = $false
 $Form.FormBorderStyle = "None"

次に、システムトレイに表示されるアイコンを作成します。以下では、PowerShellのアイコンを使用することにしました。実行時に、以下のコードが実際のシステムトレイアイコンを作成します。このアイコンは、SystrayIcon変数を設定することで、好みに応じてカスタマイズすることができます。

他の方法でアイコンをメモリに読み込むことができるSystem.Drawing.Iconクラスのドキュメントをご覧ください。

 #必要なコンポーネントの初期化/設定
 $SystrayLauncher = New-Object System.Windows.Forms.NotifyIcon
 $SystrayIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe")
 $SystrayLauncher.Icon = $SystrayIcon
 $SystrayLauncher.Text = "PowerShell Launcher"
 $SystrayLauncher.Visible = $true

スクリプトを実行すると、以下のようにPowerShellのアイコンがシステムトレイに表示されるはずです。

次に、新しいContextMenuオブジェクトを使用してメニューアイテムのコンテナを作成し、すべてのメニューアイテムを作成します。この例では、メニューには2つのスクリプトを実行するオプションと終了オプションがあります。

 $ContextMenu = New-Object System.Windows.Forms.ContextMenu
 ​
 $LoggedOnUser = New-MenuItem -Text "Get Logged On User" -MyScriptPath "C:\scripts\GetLoggedOn.ps1"
 $RestartRemoteComputer = New-MenuItem -Text "Restart Remote PC" -MyScriptPath "C:\scripts\restartpc.ps1"
 $ExitLauncher = New-MenuItem -Text "Exit" -ExitOnly

次に、作成したすべてのメニューアイテムをコンテキストメニューに追加します。これにより、各メニューオプションがフォームのコンテキストメニューに表示されるようになります。

 #コンテキストメニューにメニューアイテムを追加
 $ContextMenu.MenuItems.AddRange($LoggedOnUser)
 $ContextMenu.MenuItems.AddRange($RestartRemoteComputer)
 $ContextMenu.MenuItems.AddRange($ExitLauncher)#コンポーネントをフォームに追加
 $SystrayLauncher.ContextMenu = $ContextMenu

ランチャーフォームを表示する

フォームが完成したので、最後に行うことは、PowerShellのコンソールウィンドウが表示されないようにフォームを表示することです。これを行うには、Start-HideConsoleを使用してコンソールを非表示にし、ランチャーフォームを表示し、Start-ShowConsoleを使用してコンソールを再表示し、ハングアップしたpowershell.exeプロセスを防ぎます。

#開始
Start-HideConsole
$Form.ShowDialog() > $null
Start-ShowConsole

このようなもっと多くのヒントをお求めですか? 私の個人のPowerShellブログをチェックしてください: https://nkasco.com/FriendsOfATA

完全なコードはこちらで入手できます: https://github.com/nkasco/PSSystrayLauncher

学びどころ

おめでとうございます、このプロジェクトを完了しました! この記事で学んだこと:

  1. Windows APIのコンポーネントを公開する方法。
  2. WinFormsを介してコンテキストメニューを操作し、続くメニューアイテムを追加する方法。
  3. PowerShellでシステムトレイアイコンを作成する方法。

このプロジェクトにより、PowerShellスクリプトのための独自のシステレイメニューを作成するための理解と経験が得られるはずです!

Source:
https://adamtheautomator.com/powershell-form/