PowerShell 表单菜单:快速访问您的脚本

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

Launching PowerShell scripts via a system tray menu icon

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

环境和知识要求

在深入之前,请确保你满足以下最低要求:

  • Windows 7或更新版本
  • Windows PowerShell 3或更新版本 – 由于最近对WPF和WinForm的支持,最新版本的.NET Core 3.0与PowerShell 7预览可能在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 脚本编辑器,让我们开始吧!

在这个项目中,你将会构建三个函数:两个用于显示/隐藏控制台以提供更清晰的用户体验,一个用于向系统托盘菜单添加项目。这些函数将作为以后使用的基础,让你的生活变得更轻松,因为你将在本文稍后学到更多。

显示/隐藏控制台窗口

除非隐藏,否则当你启动 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添加一个点击事件,启动所需的脚本。在try/catch块中使用Start-Process以确保在启动脚本时(例如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"

接下来,创建将显示在系统托盘中的图标。下面我选择使用 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对象为菜单项创建容器,并创建所有菜单项。对于此示例,菜单将包含两个要运行的脚本和一个退出选项。

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