I have for a long time now wanted a tool that sat in the system tray and was always available. This PowerShell System Tray Tool would host all of my most used shortcuts. Not just shortcuts to URLs, but also elevated administrator commands on frequently used applications. So I did just this.
This tool is super fun to build yourself.
We should always start with some light garbage collection. This let’s PowerShell drop a bit of the memory when initializing the script initially. My hope is that it “eats” less memory overall.
# Force garbage collection just to start with slightly lower amount of RAM used. [System.GC]::Collect()
We are using .Net to declare the C# assemblies. We need to include these so the GUI shows up and we can control it. Technically we need this to be able to write the code that interacts and builds the GUI and control it. Like a foundation before the building.
# Declare assemblies [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null [System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | out-null [System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null
To continue We are using the .Net handler to draw an icon on the screen on startup.
# Add an icon to the systray button $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\Windows\System32\Magnify.exe")
# Create object for the systray $Systray_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
We are building the “mouseover” so you see text when you move your mouse over the drawn icon.
# Text displayed when you pass the mouse over the systray icon $Systray_Tool_Icon.Text = "TEXTTITLETEXTTITLETEXT"
More .Net here but we set the icon using the variable we created above. After we are telling the system to show the drawn icon.
# Systray icon $Systray_Tool_Icon.Icon = $icon $Systray_Tool_Icon.Visible = $true
Again more .Net
but we are using the .Net
objects to build out the menu item handlers and add text to those menu items.
# First menu displayed in the Context menu $Menu1 = New-Object System.Windows.Forms.MenuItem $Menu1.Text = "TEXTMENU1TEXT" # Second menu displayed in the Context menu $Menu2 = New-Object System.Windows.Forms.MenuItem $Menu2.Text = "TEXTMENU2TEXT" # Third menu displayed in the Context menu $Menu3 = New-Object System.Windows.Forms.MenuItem $Menu3.Text = "TEXTMENU3TEXT" # Fourth menu displayed in the Context menu $Menu4 = New-Object System.Windows.Forms.MenuItem $Menu4.Text = "TEXTMENU4TEXT"
# Fifth menu displayed in the Context menu - This will close the systray tool $Menu_Exit = New-Object System.Windows.Forms.MenuItem $Menu_Exit.Text = "Exit"
Build out the context menu for all of the menus we have above. This needs to be the same amount we have above or you will get $null variable errors.
# Create the context menu for all menus above $contextmenu = New-Object System.Windows.Forms.ContextMenu $Systray_Tool_Icon.ContextMenu = $contextmenu $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu1) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu2) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu3) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu4) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu_Exit)
# Create submenu for the menu 1 $Menu1_SubMenu1 = $Menu1.MenuItems.Add("TESTTEXT")
That is only one of the four menus we have defined above. Let’s just build out the other menu items really fast.
# Create submenu for the menu 2 $Menu2_SubMenu1 = $Menu2.MenuItems.Add("TEST2TEXT") # Create submenu for the menu 3 $Menu3_SubMenu1 = $Menu3.MenuItems.Add("TEST3TEXT") # Create submenu for the menu 4 $Menu4_SubMenu1 = $Menu4.MenuItems.Add("TEST4TEXT")
Now that we have the menu built we need to populate the button of the menu with data. That way when you click on the button it interacts in the way a user would expect it to. If you need to add more items per menu that is done like this below.
$Menu1_SubMenu2 = $Menu1.MenuItems.Add("AD Users & Computers") $Menu1_SubMenu3 = $Menu1.MenuItems.Add("Cisco AnyConnect") $Menu1_SubMenu4 = $Menu1.MenuItems.Add("Remote Viewer") $Menu1_SubMenu5 = $Menu1.MenuItems.Add("SCCM Console") $Menu1_SubMenu6 = $Menu1.MenuItems.Add("ADSI Edit") $Menu1_SubMenu7 = $Menu1.MenuItems.Add("Computer Management") $Menu1_SubMenu8 = $Menu1.MenuItems.Add("Event Viewer") $Menu1_SubMenu9 = $Menu1.MenuItems.Add("RDP / MSTSC") $Menu1_SubMenu10 = $Menu1.MenuItems.Add("Deep Freeze (C:\)")
We will need to create a .Net event per button click. That is done like this:
# Action after clicking on the Menu 1 - Submenu 1 $Menu1_SubMenu1.Add_Click({ start-process powershell })
Above, when you click on the menu 1 submenu 1 button it will open a PowerShell console. You will want to build this for every menu item you have, or want to have.
We need to add “function”, I use the term loosely as I am not building a PowerShell Function, I am building functionality for when the Exit button is clicked. To do this I open a new PowerShell window, and then hide the icon, close the window, and kill the process. I open a new PowerShell console on close as preference, I am probably going to need another console window shortly after closing this window.
# When you click Exit, this PowerShell process is terminated, and a new administrator console is started. $Menu_Exit.add_Click({ start-process powershell $Systray_Tool_Icon.Visible = $false $window.Close() # $window_Config.Close() Stop-Process $pid
If we do not hide the PowerShell console it will be in the way. You will have to minimize it. It takes up space on the taskbar, etc… thus defeating the purpose of having a system tray icon in the first place. Let’s just go ahead and hide it.
# Disappearing PowerShell Console $windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' $asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru $null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)
This is not my original code, but it works at keeping the script responsive without using RunSpaces. I am not sure where I found it, I just use it when building Forms.
# Create an application context for it to all run within. # This helps with responsiveness. $appContext = New-Object System.Windows.Forms.ApplicationContext [void][System.Windows.Forms.Application]::Run($appContext)
That is it, you now have a working system tray icon.
Below is the entire shell script we just built.
# Force garbage collection just to start slightly lower RAM usage. [System.GC]::Collect() # Declare assemblies [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null [System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null [System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | out-null [System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null # Add an icon to the systrauy button $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\Windows\System32\Magnify.exe") # Create object for the systray $Systray_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon # Text displayed when you pass the mouse over the systray icon $Systray_Tool_Icon.Text = "TEXTTITLETEXTTITLETEXT" # Systray icon $Systray_Tool_Icon.Icon = $icon $Systray_Tool_Icon.Visible = $true # First menu displayed in the Context menu $Menu1 = New-Object System.Windows.Forms.MenuItem $Menu1.Text = "TEXTMENU1TEXT" # Second menu displayed in the Context menu $Menu2 = New-Object System.Windows.Forms.MenuItem $Menu2.Text = "TEXTMENU2TEXT" # Third menu displayed in the Context menu $Menu3 = New-Object System.Windows.Forms.MenuItem $Menu3.Text = "TEXTMENU3TEXT" # Fourth menu displayed in the Context menu $Menu4 = New-Object System.Windows.Forms.MenuItem $Menu4.Text = "TEXTMENU4TEXT" # Fifth menu displayed in the Context menu - This will close the systray tool $Menu_Exit = New-Object System.Windows.Forms.MenuItem $Menu_Exit.Text = "Exit" # Create the context menu for all menus above $contextmenu = New-Object System.Windows.Forms.ContextMenu $Systray_Tool_Icon.ContextMenu = $contextmenu $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu1) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu2) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu3) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu4) $Systray_Tool_Icon.contextMenu.MenuItems.AddRange($Menu_Exit) # Create submenu for the menu 1 $Menu1_SubMenu1 = $Menu1.MenuItems.Add("TEST1TEXT") # Create submenu for the menu 2 $Menu2_SubMenu1 = $Menu2.MenuItems.Add("TEST2TEXT") # Create submenu for the menu 3 $Menu3_SubMenu1 = $Menu3.MenuItems.Add("TEST3TEXT") # Create submenu for the menu 4 $Menu4_SubMenu1 = $Menu4.MenuItems.Add("TEST4TEXT") # Action after clicking on the Menu 1 - Submenu 1 $Menu1_SubMenu1.Add_Click({ start-process powershell }) # Action after clicking on the Menu 2 - Submenu 1 $Menu1_SubMenu1.Add_Click({ start-process "C:\Windows\system32\dsa.msc" }) # Action after clicking on the Menu 3 - Submenu 1 $Menu1_SubMenu1.Add_Click({ start-process cmd }) # When Exit is clicked, close everything and kill the PowerShell process $Menu_Exit.add_Click({ start-process powershell $Systray_Tool_Icon.Visible = $false $window.Close() # $window_Config.Close() Stop-Process $pid # Make PowerShell Disappear $windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' $asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru $null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0) # Create an application context for it to all run within. # This helps with responsiveness, especially when clicking Exit. $appContext = New-Object System.Windows.Forms.ApplicationContext [void][System.Windows.Forms.Application]::Run($appContext)
I had a ton of fun writing this and hope you find it useful. If you did or have any questions feel free to reach out.
-iNet