PowerShell表單菜單:快速訪問您的腳本

又到了周末项目时间,今天你将学习如何构建一个轻量级的系统托盘 PowerShell 表单菜单,可以快速、方便地启动你最喜欢的 PowerShell 脚本。你可以在下面看到最终结果。

Launching PowerShell scripts via a system tray menu icon

在本文中,你将逐步学习如何构建自己的 PowerShell 菜单 GUI。

环境和知识要求

在开始之前,请确保满足以下最低要求:

  • Windows 7 或更高版本
  • Windows PowerShell 3 或更高版本 – 最新版本的 .NET Core 3.0 预览版可能适用于 Windows,因为最近添加了对 WPF 和 WinForm 的支持,但未经测试。
  • .NET Framework 4.5 或更高版本
  • A familiarity with Windows Forms (WinForms)  You can, however, due this with WPF too though.

对于这个项目,好消息是你实际上不需要依赖于Visual StudioPoshGUI或任何其他 UI 开发工具,因为该项目的主要组件将依赖于以下内容:

  • NotifyIcon – 这将代表我们可自定义的系统托盘图标,供用户与之交互。
  • 上下文菜單 – 當用戶在系統托盤圖標上右鍵單擊時的容器。
  • 菜單項目 – 右鍵菜單中的每個選項的個別對象。

打開你最喜歡的PowerShell腳本編輯器,我們開始吧!

在這個項目中,你將構建三個函數:兩個函數用於顯示/隱藏控制台,以提供更清晰的用戶體驗,另一個函數用於將項目添加到系統托盤菜單中。這些函數將作為以後使用的基礎,讓你的生活變得更輕鬆,稍後你將在本文中學到更多。

顯示/隱藏控制台窗口

除非隱藏,否則當你運行PowerShell腳本時,熟悉的PowerShell控制台將彈出。由於你將創建的PowerShell表單中的菜單項目將啟動腳本,你應該確保控制台不會彈出,只需執行腳本。

當腳本執行時,你可以使用一個小的.NET切換PowerShell控制台窗口的顯示或隱藏。

首先将Window .NET类型添加到当前会话中。为此,您将使用一些C#代码,如下所示。您需要加载到上下文中的两个方法是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()方法。

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

有了这两个函数,您现在可以随意显示或隐藏控制台窗口。

注意:如果您想要查看通过菜单执行的脚本的输出,您可以使用PowerShell记录或其他基于文本的日志记录功能。这使您能够保持控制,而不仅仅是使用WindowStyle参数运行PowerShell会话来隐藏窗口。

现在开始构建脚本代码,调用Start-HideConsole。当PowerShell表单菜单驱动的脚本执行时,这将确保PowerShell控制台窗口不会弹出。

<# 
	初始化函数和对象加载到内存中
	显示基于文本的加载条或向主机写入进度
#>
 
Start-HideConsole
 
<# 
	代码以显示您的表单/系统托盘图标
	这将保持控制台在此处直到关闭
 #>

创建菜单选项

现在是创建菜单选项的时候了。为了确保您以后可以轻松创建新选项,创建另一个函数,这次称为New-MenuItem。当您调用此函数时,它将创建一个新的MenuItem .NET对象,然后您可以将其添加到菜单中。

每个菜单选项将启动另一个脚本或退出启动器。为了适应此功能,New-MenuItem函数有三个参数:

  • 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在try/catch區塊內可以提供一種清晰的方式,以便您確保任何啟動腳本時的錯誤(例如PowerShell不可用或提供的路徑上不存在腳本)都落在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
 }

通过将上面的代码复制并粘贴到PowerShell控制台中,并运行该函数提供一些虚拟的参数值,来测试New-MenuItem函数。您将看到返回了一个.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"

接下來,創建將出現在系統托盤中的圖標。在運行時,下面的代碼將創建一個實際的系統托盤圖標。通過將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對象為菜單項創建一個容器,並創建所有菜單項。對於此示例,菜單將包含兩個要運行的腳本和一個退出選項。

 $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/