Reputation: 8042
I have working code which will change tray icon depending on notepad process existence.
Set-StrictMode -Version Latest
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function menu1clickEvent ($label) {
# Build Form object
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(200,200)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
$Form.Controls.Add($Label) # Add label to form
$form.ShowDialog()| Out-Null # Show the Form
}
function menu4clickEvent ($icon) {
$icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
function create_taskbar_menu($label){
# Create menu item
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
# Create menu item
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Exit"
# Add menu items to context menu
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem1)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem2)
# Add click events
$MenuItem1.add_Click({menu1clickEvent $label})
$MenuItem2.add_Click({menu4clickEvent $Main_Tool_Icon})
}
# Add assemblies for WPF and Mahapps
[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
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:/WINDOWS/system32/notepad.exe")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:/WINDOWS/system32/resmon.exe")
# create tray icon
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = if (Test-Notepad) { $onlineIcon } else { $offlineIcon }
$Main_Tool_Icon.Visible = $true
# Build Label object
$Label = New-Object System.Windows.Forms.Label
$Label.Name = "labelName"
$Label.AutoSize = $True
# Initialize the timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
if ($Label){
$Label.Text, $Main_Tool_Icon.Icon = if (Test-Notepad) {
"Notepad is running", $onlineIcon
} else {
"Notepad is NOT running", $offlineIcon
}
}
})
$timer.Start()
create_taskbar_menu $label
# ERROR:
# create_taskbar_menu2 $label $Main_Tool_Icon
# Make PowerShell Disappear - Thanks Chrissy
$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)
# Use a Garbage colection to reduce Memory RAM
# https://dmitrysotnikov.wordpress.com/2012/02/24/freeing-up-memory-in-powershell-using-garbage-collector/
# https://learn.microsoft.com/fr-fr/dotnet/api/system.gc.collect?view=netframework-4.7.2
[System.GC]::Collect()
# Create an application context for it to all run within - Thanks Chrissy
# This helps with responsiveness, especially when clicking Exit - Thanks Chrissy
$appContext = New-Object System.Windows.Forms.ApplicationContext
try
{
[System.Windows.Forms.Application]::Run($appContext)
}
finally
{
foreach ($component in $timer, $Main_Tool_Icon, $offlineIcon, $onlineIcon, $appContext)
{
# The following test returns $false if $component is
# $null, which is really what we're concerned about
if ($component -is [System.IDisposable])
{
$component.Dispose()
}
}
Stop-Process -Id $PID
}
My goal is to refactor create_taskbar_menu
method to use passed variables and not the global one, here is my code:
function create_taskbar_menu2($label, $trayIcon){
# Create menu item
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
# Create menu item
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Exit"
# Add menu items to context menu
$contextmenu = New-Object System.Windows.Forms.ContextMenu
# Set icton properties
$trayIcon.ContextMenu = $contextmenu
$trayIcon.contextMenu.MenuItems.AddRange($MenuItem1)
$trayIcon.contextMenu.MenuItems.AddRange($MenuItem2)
# Add click events
$MenuItem1.add_Click({menu1clickEvent $label})
$MenuItem2.add_Click({menu4clickEvent $trayIcon})
}
I call this method as create_taskbar_menu2 $label $Main_Tool_Icon
instead of create_taskbar_menu $label
. Problem is that after thisrefactor my application crashes, when I click on taskbar exit button. Following message appears:
Unhandled exception has occurred in your application. If you click Continue, the application will ignore this error and attempt to continue. If you click Quit, the application will close immediately.
The variable '$trayIcon' cannot be retrieved because it has not been set.
I run my code from cmd.exe
as powershell -f script.ps1
And I get nothing to console. What I'm, doing wrong and how can I debug such .NET Framework exceptions?
PS: here is log which I can copy from dialog error message box (it is a bit longer so I've pasted it on pastebin)
Upvotes: 0
Views: 412
Reputation: 9975
This answer doesn't address your question, but it does fix your problem :-)...
Inside your create_taskbar_menu2
function, change:
$MenuItem2.add_Click({menu4clickEvent $trayIcon})
to
$MenuItem2.add_Click({
menu4clickEvent $trayIcon
}.GetNewClosure()
)
I think what's happening is that the callback ScriptBlock - i.e. { menu4clickEvent $trayIcon }
- is being executed in a separate scope to your function, and there's no variable called $trayIcon
in scope when the ScriptBlock gets executed.
The call to GetNewClosure()
creates a new scriptblock and "binds" (for want of a better word - not sure of the exact terminology) the current value of the function's $trayIcon
parameter to the $trayIcon
variable inside the ScriptBlock's scope, so now when your callback gets executed, $trayIcon
has the value you'd expect it to have.
To see a smaller example of this in action, compare these two scripts:
Set-StrictMode -Version "Latest";
function Get-MyCallback1
{
param( $x )
return { write-host $x }
}
$callback = Get-MyCallback1 "aaa"
$callback.Invoke()
which fails with:
Exception calling "Invoke" with "0" argument(s): "The variable '$x' cannot be retrieved because it has not been set."
At line:1 char:1
+ $callback.Invoke()
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : RuntimeException
versus
Set-StrictMode -Version "Latest";
function Get-MyCallback2
{
param( $x )
return { write-host $x }.GetNewClosure()
}
$callback = Get-MyCallback2 "aaa"
$callback.Invoke()
which outputs:
aaa
As for debugging PowerShell, I've never tried it, but may try what @Lex Li suggested - attach a debugger (e.g. using Visual Studio -> Debug -> Attach to Process...) to the running instance of powershell.exe and see if it triggers debugging when your exception happens...
Upvotes: 1