Reputation: 470
I have the following NotifyIcon running using PowerShell:
This is the context menu opened by right clicking the icon, it's just showing exit at the moment:
I would like to know how I can add two event handlers:
I've been searching the web for over an hour and I have tried about 20 different variations using old code from 5 or 6 different websites (all showing wildly different examples). I have gained nothing but a headache. Can anyone offer any guidance/direction?
$ProgramDataPath = "$ENV:ProgramData\test"
$ProgramFilesPath = "${env:ProgramFiles(x86)}\test"
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$STForm = New-Object System.Windows.Forms.form
$NotifyIcon = New-Object System.Windows.Forms.NotifyIcon
$ContextMenu = New-Object System.Windows.Forms.ContextMenu
$MenuItem = New-Object System.Windows.Forms.MenuItem
$Timer = New-Object System.Windows.Forms.Timer
$HealthyIcon = New-Object System.Drawing.Icon("$ProgramFilesPath\icons\healthy.ico")
$UnhealthyIcon = New-Object System.Drawing.Icon("$ProgramFilesPath\icons\unhealthy.ico")
$STForm.ShowInTaskbar = $false
$STForm.WindowState = "minimized"
$NotifyIcon.Icon = $HealthyIcon
$NotifyIcon.ContextMenu = $ContextMenu
$NotifyIcon.ContextMenu.MenuItems.AddRange($MenuItem)
$NotifyIcon.Visible = $True
# We need to avoid using Start-Sleep as this freezes the GUI. Instead, we'll utilitse the .NET forms timer, it repeats a function at a set interval.
$Timer.Interval = 300000 # (5 min)
$Timer.add_Tick({ Load-Config })
$Timer.start()
# This will appear as a right click option on the system tray icon
$MenuItem.Index = 0
$MenuItem.Text = "Exit"
$MenuItem.add_Click({
$Timer.Stop()
$NotifyIcon.Visible = $False
$STForm.close()
})
function Load-Config
{
#Get-Content some Data from a file here
if ($warn)
{
$NotifyIcon.Icon = $UnhealthyIcon
$NotifyIcon.ShowBalloonTip(30000, "Attention!", "Some data from a file here...", [system.windows.forms.ToolTipIcon]"Warning")
Remove-Variable warn
}
else
{
$NotifyIcon.Icon = $HealthyIcon
}
}
Load-Config
[void][System.Windows.Forms.Application]::Run($STForm)
Upvotes: 2
Views: 7542
Reputation: 6860
Lets talk about what you really need. It looks like you have alot of unneeded parts like a timer and so on. All you need is a runspace. A Open Form will keep the runspace open without the need for that timer. Make sure the $Form.ShowDialog()
is the last thing run.
So lets move on to the NotifyIcon popup. The method that makes that popup happen is private, which means we will need to reach it through reflection. We will also need to set the event for the Notify Icon to run on MouseDown
as well as get the button clicked $_.button
Make sure you set the $NotifyIcon.Icon
to a Icon or else the Notify Icon wont show up.
Working Script
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ShowInTaskbar = $true
$form.WindowState = [System.Windows.WindowState]::Normal
$MenuItemLeft = New-Object System.Windows.Forms.MenuItem
$MenuItemLeft.Text = "Left Exit"
$MenuItemLeft.add_Click({
$NotifyIcon.Visible = $False
$form.Close()
$NotifyIcon.Dispose()
})
$ContextMenuLeft = New-Object System.Windows.Forms.ContextMenu
$ContextMenuLeft.MenuItems.Add($MenuItemLeft)
$MenuItemRight = New-Object System.Windows.Forms.MenuItem
$MenuItemRight.Text = "Right Exit"
$MenuItemRight.add_Click({
$NotifyIcon.Visible = $False
$form.Close()
$NotifyIcon.Dispose()
})
$ContextMenuRight = New-Object System.Windows.Forms.ContextMenu
$ContextMenuRight.MenuItems.Add($MenuItemRight)
$NotifyIcon= New-Object System.Windows.Forms.NotifyIcon
$NotifyIcon.Icon = "C:\Test\Test.ico"
$NotifyIcon.ContextMenu = $ContextMenuRight
$NotifyIcon.add_MouseDown({
if ($_.Button -eq [System.Windows.Forms.MouseButtons]::left ) {
$NotifyIcon.contextMenu = $ContextMenuLeft
}else{
$NotifyIcon.contextMenu = $ContextMenuRight
}
$NotifyIcon.GetType().GetMethod("ShowContextMenu",[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic).Invoke($NotifyIcon,$null)
})
$NotifyIcon.Visible = $True
$form.ShowDialog()
$NotifyIcon.Dispose()
I Reread your post so ill provide you with the most important parts
For Powershell to run your commands a runspace must be active. A Runspace takes powershell commands and turns them into real actions.
Since you went with powershell for this then the notifyicons actions are dependent on a runspace to interpret those actions.
A NotifyIcon is basically just a icon in the corner that can popup a balloon notification or Context Menu.
So when you look you will see $NotifyIcon.ContextMenu
that is a property that holds a ContextMenu Object. A Context Menu Object contains Menu Items.
So just add MenuItems to a ContextMenu Object and then add that ContextMenu Object to $NotifyIcon.ContextMenu. Now you can change and add all the items you like.
Since powershell waits for the form to close before moving on to the next line of code the $Form.ShowDialog()
will keep the runspace alive until the form is exited.
Lets look at this nasty mess :
$NotifyIcon.GetType().GetMethod("ShowContextMenu",[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic).Invoke($NotifyIcon,$null)
This is called reflection. It allows you to interact with a class. In easier terms. ShowContextMenu method is private and cant be run normally from outside the internal workings of the class. Using reflection you can call it anyways.
So lets break it down just a little more since this is really what you asked about.
GetType() will get you what the object is. If i did "HEY".gettype()
it would tell me that this object was a string. In this case $NotifyIcon.GetType()
is telling me that this is a NotifyIcon. Whats happening its its bring me back a Type Class.
In this we see GetMethod("ShowContextMenu")
but let me dig a little deeper here... How did we know there was a method called ShowContextMenu. Well what we can do is view all the members of this NotifyIcon class by using GetMembers()
. Now GetMembers()
is really just a search... by default only searches for public Members so we needs to search all Members. The parameters of what to search for is in an enum [System.Reflection.BindingFlags]
and some Bitwise math.
$BitWise
[System.Reflection.BindingFlags].GetEnumNames() | %{
$BitWise = $BitWise -bor [System.Reflection.BindingFlags]$_
} | out-null
$NotifyIcon.GetType().GetMembers($BitWise) | ?{$_.Name -like "*Context*"} | select Name, MemberType
This says find all items that contain the word Context in its name and display its Full name and what Type it is. In response we get
Name MemberType
---- ----------
set_ContextMenu Method
get_ContextMenu Method
get_ContextMenuStrip Method
set_ContextMenuStrip Method
ShowContextMenu Method
ContextMenu Property
ContextMenuStrip Property
contextMenu Field
contextMenuStrip Field
We can see ShowContextMenu and we can also see its a method
So now we need to get that method directly. Here comes getMethod()
which is just another search that brings back 1 item instead of all the items. So
GetMethod("ShowContextMenu",[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic)
Get Method ShowContextMenu it will be Private so NonPublic and a instance of the class has to be created before it can run so Instance.
.Invoke($NotifyIcon,$null)
Then we invoke the method by telling it which control has the method we want to run and pass any parameters which are none so $null.
And thats how you do it.
Upvotes: 4