It's weekend project time again and today you will learn how to build a lightweight system tray context menu where you can quickly and easily launch your most coveted PowerShell scripts. You can see below the end result.

Launching PowerShell scripts via a system tray menu icon

In this article, you'll learn how to build your own PowerShell menu GUI by breaking the process down step-by-step.

Environment and Knowledge Requirements

Before you dive in, please be sure you meet the following minimum requirements:

For this project, the good news is that you won't really need to rely on Visual Studio, PoshGUI, or any other UI development tool as the primary components that this project will rely on the following:

  • NotifyIcon - This will represent our customizable system tray icon for the user to interact with.
  • ContextMenu - Container for when the user right-clicks on the tray icon.
  • MenuItem - Individual objects for each option within the right-click menu.

Open up your favorite PowerShell script editor and let's get started!

For this project you are going to build three functions: two functions to show/hide the console to provide a cleaner user experience and one to add items to your systray menu. These functions will serve as a foundation for later use to make your life much easier as you will learn a bit later in this article.

Show/Hide Console Window

Unless hidden, when you launch a PowerShell script, the familiar PowerShell console will come up. Since the menu items you'll create will launch scripts, you should ensure the console doesn't up. You just want it to execute.

When a script is executed, you can toggle the PowerShell console window showing or not using a little .NET.

First add the Window .NET type into the current session. To do this, you'll use some C# as you'll see below. The two methods you need to load into context are GetConsoleWindow and ShowWindow. By loading these DLLs into memory you are exposing certain parts of the API, this allows you to use them in the context of your PowerShell script:

 #Load dlls into context of the current console session
 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);
 '

Create two functions using the loaded above using the GetConsoleWindow() and ShowWindow() method as shown below.

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

With these two functions you now have created a way in which you can show or hide the console window at will.

Note: If you'd like to see output from the scripts executed via the menu, you can use PowerShell transcripts or other text-based logging features. This allows you to maintain control versus only running the PowerShell session with the WindowStyle parameter to hide.

Now begin building script code by calling Start-HideConsole. When the menu-driven script executes, this will ensure the PowerShell console window doesn't come up.

<# 
	Initialization of functions and objects loading into memory
	Display a text-based loading bar or Write-Progress to the host
#>
 
Start-HideConsole
 
<# 
	Code to display your form/systray icon
	This will hold the console here until closed
 #>

Create Menu Options

Now it's time to create the menu options. Ensuring you can easily create new options later on, create another function this time called New-MenuItem. When you call this function, it will create a new MenuItem .NET object which you can then add to the menu later.

Each menu option will launch another script or exit the launcher. To accommodate for this functionality,  the New-MenuItem function has three parameters:

  • Text - The label the user will click on
  • MyScriptPath - The path to the PowerShell script to execute
  • ExitOnly - The option to exit the launcher.

Add the below function snippet to the menu script.

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

Continuing on building the New-MenuItem function, create a MenuItem object by assigning it to a variable.

 #Initialization
 $MenuItem = New-Object System.Windows.Forms.MenuItem

Next, assign the text label to the menu item.

 # Apply desired text
 if($Text) {
 	$MenuItem.Text = $Text
 }

Now add a custom property to the MenuItem called MyScriptPath. This path will be called upon when the item is clicked in the menu.

 #Apply click event logic
 if($MyScriptPath -and !$ExitOnly){
 	$MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty

Add a click event to the MenuItem that launches the desired script. Start-Process provides a clean way to do this within a try/catch block so that you can make sure any errors launching the script (such as PowerShell not being available or the script not existing at the provided path) fall to your catch block.

   $MenuItem.Add_Click({
        try{
            $MyScriptPath = $This.MyScriptPath #Used to find proper path during click event
            
            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
        }
  })

Sdd the remaining logic to provide an exit condition for the launcher followed by returning your newly created MenuItem back to be assigned to another variable at runtime.

    #Provide a way to exit the launcher
    if($ExitOnly -and !$MyScriptPath){
        $MenuItem.Add_Click({
            $Form.Close()
    
            #Handle any hung processes
            Stop-Process $PID
        })
    }
 
 	 #Return our new MenuItem
    $MenuItem
 }

You should now have the New-MenuItem function created! The final function should look like this:

 function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )
 
     #Initialization
     $MenuItem = New-Object System.Windows.Forms.MenuItem
 
     #Apply desired text
     if($Text){
         $MenuItem.Text = $Text
     }
 
     #Apply click event logic
     if($MyScriptPath -and !$ExitOnly){
         $MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty
     }
 
     $MenuItem.Add_Click({
             try{
                 $MyScriptPath = $This.MyScriptPath #Used to find proper path during click event
             
                 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
             }
         })
 
     #Provide a way to exit the launcher
     if($ExitOnly -and !$MyScriptPath){
         $MenuItem.Add_Click({
                 $Form.Close()
    
                 #Handle any hung processes
                 Stop-Process $PID
             })
     }
 
     #Return our new MenuItem
     $MenuItem
 }

Test the New-MenuItem function by copying and pasting the above code into your PowerShell console and running the function providing some fake parameter values. You'll see that a .NET MenuItem object is returned.

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

Creating A Launcher Form

Want more tips like this? Check out my personal PowerShell blog at: https://nkasco.com/FriendsOfATA

Now that you can easily create new menu items, it's time to create a system tray launcher which will display the menu.

Create a basic form object to add components to. This doesn't need to be anything fancy as it will be hidden to the end user and will keep the console running in the background as well.

 #Create Form to serve as a container for our components
 $Form = New-Object System.Windows.Forms.Form
 ​
 #Configure our form to be hidden
 $Form.BackColor = "Magenta" #Match this color to the TransparencyKey property for transparency to your form
 $Form.TransparencyKey = "Magenta"
 $Form.ShowInTaskbar = $false
 $Form.FormBorderStyle = "None"

Next, create the icon that will show up in the system tray. Below I've chosen to use the PowerShell icon. At runtime, the below code creates an actual system tray icon. This icon can be customized to your liking by setting the SystrayIcon variable to your desired icon.

Check out the documentation for the System.Drawing.Icon class to see other methods in which you can load an icon into memory.

 #Initialize/configure necessary components
 $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

When the script is run, you should then see a PowerShell icon show up in your system tray as you can see below.

Now, create a container for your menu items with a new ContextMenu object and create all of your menu items. For this example, the menu will have two scripts to run and an exit option.

 $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

Next, add all of the menu items just created to the context menu. This will ensure each menu option shows up in the form context menu.

 #Add menu items to context menu
 $ContextMenu.MenuItems.AddRange($LoggedOnUser)
 $ContextMenu.MenuItems.AddRange($RestartRemoteComputer)
 $ContextMenu.MenuItems.AddRange($ExitLauncher)
 ​
 #Add components to our form
 $SystrayLauncher.ContextMenu = $ContextMenu

Show the Launcher Form

Now that the form is complete, the last thing to do is to show it while ensuring the PowerShell console window doesn't come up. Do this by using your Start-HideConsole , displaying the launcher form and then showing the console again withStart-ShowConsole to prevent a hung powershell.exe process.

#Launch
Start-HideConsole
$Form.ShowDialog() > $null
Start-ShowConsole
Want more tips like this? Check out my personal PowerShell blog at: https://nkasco.com/FriendsOfATA

The full code in its entirety can be found here: https://github.com/nkasco/PSSystrayLauncher

Your Takeaways

Congrats, you've finished this project! In this article you learned:

  1. How to expose components of the Windows API.
  2. How to work with context menus via WinForms and add subsequent menu items.
  3. How to create a system tray icon in PowerShell.

This Project should give you enough understanding and experience to create your own systray menu for your PowerShell scripts!