change tray icon based on event - winforms

I have code that will start process in tray (task bar). After right click on tray icon it will show menu. After clicking on first menu item winform window starts. This winform shows status about the notepad process. My goal is to change the tray icon based on the status of notepad (if notepad is running then show online.ico otherwise show offline.ico). If I understand correct then my code is starting/stopping System.Windows.Forms.Timer every time winform window is opened/closed and I'm not sure if this is the best possible approach. My guess is that I need to somehow start timer "outside" of OnMenuItem1ClickEventFn so it can somehow reload *.ico files. Following script is heavily inspired by this site:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function OnMenuItem1ClickEventFn () {
# Build Label object
$Label = New-Object System.Windows.Forms.Label
$Label.Name = "labelName"
$Label.AutoSize = $True
# Set and start timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
if ($Label){
$Label.Text = if (Test-Notepad) { "Notepad is running" } else { "Notepad is NOT running" }
}
})
$timer.Start()
# 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.Add_Closing({ $timer.Dispose() }) # Dispose() also stops the timer.
$Form.Controls.Add($Label) # Add label to form
$form.ShowDialog()| Out-Null # Show the Form
}
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
$window.Close()
Stop-Process $pid
}
function create_taskbar_menu{
# Create menu items
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = $icon
$Main_Tool_Icon.Visible = $true
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Menu Item 2"
$MenuItem3 = New-Object System.Windows.Forms.MenuItem
$MenuItem3.Text = "Menu Item 3"
$MenuItem4 = New-Object System.Windows.Forms.MenuItem
$MenuItem4.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)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem3)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem4)
$MenuItem4.add_Click({OnMenuItem4ClickEventFn})
$MenuItem1.add_Click({OnMenuItem1ClickEventFn})
}
$Current_Folder = split-path $MyInvocation.MyCommand.Path
# 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
# [System.Reflection.Assembly]::LoadFrom("Current_Folder\assembly\MahApps.Metro.dll") | out-null
# Choose an icon to display in the systray
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
# $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
create_taskbar_menu
# 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
[void][System.Windows.Forms.Application]::Run($appContext)
EDITED: working solution based on #BACON answer
# Toggle following two lines
Set-StrictMode -Version Latest
# Set-StrictMode -Off
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function OnMenuItem1ClickEventFn () {
# 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 OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
function create_taskbar_menu{
# Create menu items
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Menu Item 2"
$MenuItem3 = New-Object System.Windows.Forms.MenuItem
$MenuItem3.Text = "Menu Item 3"
$MenuItem4 = New-Object System.Windows.Forms.MenuItem
$MenuItem4.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)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem3)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem4)
$MenuItem4.add_Click({OnMenuItem4ClickEventFn})
$MenuItem1.add_Click({OnMenuItem1ClickEventFn})
}
$Current_Folder = split-path $MyInvocation.MyCommand.Path
# 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
# [System.Reflection.Assembly]::LoadFrom("Current_Folder\assembly\MahApps.Metro.dll") | out-null
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
$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
# 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
}

You already have code to set the icon used by the NotifyIcon...
$Main_Tool_Icon.Icon = $icon
...and to define the icons to use...
# Choose an icon to display in the systray
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
# $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
...and to periodically test if Notepad is running and respond appropriately...
$timer.Add_Tick({
if ($Label){
$Label.Text = if (Test-Notepad) { "Notepad is running" } else { "Notepad is NOT running" }
}
})
You just need to combine them with a couple additional tweaks...
Store each icon in its own variable with a descriptive name so its easy to switch between them.
$Main_Tool_Icon needs to be defined outside the scope of create_taskbar_menu so it can be accessed inside of OnMenuItem1ClickEventFn. I moved the creation and initialization to just before the call to create_taskbar_menu, but you could also initialize it inside of create_taskbar_menu or some other function.
That ends up looking like this...
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
$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
create_taskbar_menu
...and this...
$timer.Add_Tick({
if ($Label){
# Change the text and icon with one test
$Label.Text, $Main_Tool_Icon.Icon = if (Test-Notepad) {
"Notepad is running", $onlineIcon
} else {
"Notepad is NOT running", $offlineIcon
}
}
})
You'll see that an icon is selected based on the result of Test-Notepad both when $Main_Tool_Icon is initialized and when the Tick event is raised.
As for disposing $timer when $Form is closing...
$Form.Add_Closing({ $timer.Dispose() })
...that is almost an appropriate place to do that, however...
The Closing event is raised essentially when the form has been requested to close; it provides an opportunity for that request to be canceled. The Closed event would be more appropriate because it is raised when the form has actually been closed.
The documentation states that both the Closing and Closed events are obsolete. Use the FormClosed event instead.
Also, in OnMenuItem4ClickEventFn you are calling $window.Close() even though $window is not defined; I think you meant $Form.Close(). An alternative that I think is a bit cleaner would be to have clicking the Exit menu item merely exit the Windows Forms application loop...
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
...and then you can put your cleanup/teardown code in a finally block at the end of the script...
try
{
# This call returns when [System.Windows.Forms.Application]::Exit() is called
[System.Windows.Forms.Application]::Run($appContext)
}
finally
{
# $timer would also have to be defined at the script scope
# (outside of OnMenuItem1ClickEventFn) for this to work
$timer.Dispose()
# $Form, $Label, $Main_Tool_Icon, $onlineIcon, etc. would all be candidates for disposal...
# Exit the entire PowerShell process
Stop-Process $pid
}
The following is complete code that incorporates the changes mentioned above and works for me...
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function OnMenuItem1ClickEventFn () {
$timer.Start()
# 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.Add_Closing({ $timer.Dispose() }) # Dispose() also stops the timer.
$Form.Controls.Add($Label) # Add label to form
$form.ShowDialog()| Out-Null # Show the Form
}
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
function create_taskbar_menu{
# Create menu items
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Menu Item 2"
$MenuItem3 = New-Object System.Windows.Forms.MenuItem
$MenuItem3.Text = "Menu Item 3"
$MenuItem4 = New-Object System.Windows.Forms.MenuItem
$MenuItem4.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)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem3)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem4)
$MenuItem4.add_Click({OnMenuItem4ClickEventFn})
$MenuItem1.add_Click({OnMenuItem1ClickEventFn})
}
$Current_Folder = split-path $MyInvocation.MyCommand.Path
# 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
# [System.Reflection.Assembly]::LoadFrom("Current_Folder\assembly\MahApps.Metro.dll") | out-null
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
$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
}
}
})
create_taskbar_menu
# 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, $form, $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
}

Related

Powershell Forms - How to add context menu event handler to prevent open unless item selected?

I have a form with a listview for which I have a right click context menu. I'd like the menu to only appear if the right clikc is above the selected item in the listview - at the moment it appears anywhere in the listview windows and even if nothing is selected.
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.Text = "Foo"
$form.Size = '400,400'
$CMitemEnable = New-Object System.Windows.Forms.ToolStripMenuItem
$CMitemEnable.Text = 'Enable'
$CMitemDisable = New-Object System.Windows.Forms.ToolStripMenuItem
$CMitemDisable.Text = 'Disable'
$lvcontextmenu = New-Object System.Windows.Forms.ContextMenuStrip
$lvcontextmenu.ShowImageMargin = $false
$lvcontextmenu.Items.AddRange(#($CMitemEnable,$CMitemDisable))
$listviewbox = New-Object System.Windows.Forms.ListView
$listviewbox.View = [System.Windows.Forms.View]::Details
$listviewbox.Location = '15,20'
$listviewbox.Size = '340,150'
$listviewbox.Columns.Add('Host',180) | Out-Null
$listviewbox.FullRowSelect = $true
$listviewbox.MultiSelect = $false
# $listviewbox.ContextMenuStrip = $lvcontextmenu
$InfoText = New-Object System.Windows.Forms.TextBox
$InfoText.Location = '15,180'
$InfoText.Size = '340,120'
$InfoText.Multiline = $true
$InfoText.ScrollBars = "Vertical"
$InfoText.ReadOnly = $true
$form.Controls.AddRange(#($listviewbox,$InfoText))
$listviewbox.Items.AddRange(#('Foobar1','foobar2'))
$listviewbox.Add_MouseDown({
$listviewbox.contextmenustrip = $null
If($listviewbox.SelectedItems.Count){
$listviewbox.contextmenustrip = $lvcontextmenu
$InfoText.AppendText("`r`n" + ($this.SelectedItems[0].Text))
}
Else {
$InfoText.AppendText("`r`nNothing selected")
$listviewbox.contextmenustrip = $null
}
})
# Show form
$form.ShowDialog() | Out-Null
$form.Dispose()
I've not done context menus before and can't see for the life of me how to implement the event handler for this - there's examples for C etc but can someone point me in the right direction for Powershell please?
Considering the following points:
Don't set ContextMenuStrip for the ListView
Handle MouseClick event, so first the item will be selected, then your code run. (As an alternative, if you prefer mouse down, then you need to need to put your logic in BeginInvoke to make sure it will run after selection of item.)
Using GetBound method of the selected item and e.Location, check if the clicked location is inside the item rectangle
Show the ContextMenuStrip using Show by passing Cursor.Position
Here is a working example:
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.Text ="Test"
$form.Controls.AddRange(#(
($listView1 = [System.Windows.Forms.ListView] #{
Dock = [System.Windows.Forms.DockStyle]::Fill;
FullRowSelect = $true;
View = [System.Windows.Forms.View]::Details;
})
))
$contextMenuStrip1 = [System.Windows.Forms.ContextMenuStrip]#{}
$listView1.Columns.Add("C1") | Out-Null
$listView1.Columns.Add("C2") | Out-Null
$listView1.Items.Add("Item1") | Out-Null
$listView1.Items.Add("Item2") | Out-Null
$contextMenuStrip1.Items.Add("Menu 1") | Out-Null
$contextMenuStrip1.Items.Add("Menu 2") | Out-Null
$listView1.Add_MouseClick({param($sender,$e)
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Right){
if ($listView1.FocusedItem.GetBounds(
[System.Windows.Forms.ItemBoundsPortion]::Entire).Contains($e.Location)){
$contextMenuStrip1.Show([System.Windows.Forms.Cursor]::Position)
}
}
})
$form.ShowDialog() | Out-Null
$form.Dispose()
$contextMenuStrip1.Dispose()
Maybe there is a cleaner way, but this works for me:
First, remove line 22 ($listviewbox.ContextMenuStrip = $lvcontextmenu)
and then change the Click-handler for the $listviewbox into:
$listviewbox.Add_Click({
If($this.SelectedItems.Count){
$this.ContextMenuStrip = $lvcontextmenu
$InfoText.AppendText("`r`n" + ($this.SelectedItems[0].Text))
}
Else {
$this.ContextMenuStrip = $null
$InfoText.AppendText("`r`nNothing selected")
}
})
The following event handler works as intended - a combination of hints from both of you, thanks.
$listviewbox.Add_MouseUp({
$listviewbox.contextmenustrip = $null
If($listviewbox.SelectedItems.Count){
$listviewbox.contextmenustrip = $lvcontextmenu
$InfoText.AppendText("`r`n" + ($this.SelectedItems[0].Text))
}
Else {
$InfoText.AppendText("`r`nNothing selected")
$listviewbox.contextmenustrip = $null
}
})

Powershell - how do I change a variable from a listbox item to a combobox item?

I'm attempting to clean up my GUI by removing listboxes of items that users can select to a constrained combobox (using DropDownList to lock user-input) in Powershell. Thus far, I haven't been able to get the variable to reflect the combobox item that is chosen.
Ideally, I want to get $Env to equal the text string chosen in the Combobox replacing $listbox
I attempted trying to follow powershell combobox items to variable to no avail as I don't understand how to use the "SelectedIndexChanged" event...I may just not fully understand the syntax on how to use this...a code example would be awesome.
The current code I have:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = "Select an environment"
$form.Size = New-Object System.Drawing.Size(190,250)
$form.StartPosition = "CenterScreen"
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(10,180)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $OKButton
$form.Controls.Add($OKButton)
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.Text = "Please select an environment:"
$form.Controls.Add($label)
$listBox = New-Object System.Windows.Forms.ListBox
$listBox.Location = New-Object System.Drawing.Point(10,40)
$listBox.Size = New-Object System.Drawing.Size(150,20)
$listBox.Height = 140
[void] $listBox.Items.AddRange(#("PROD", "QA1", "QA2", "TR"))
$form.Controls.Add($listBox)
$form.Topmost = $True
do
{
$result = $form.ShowDialog()
if ($ListBox.SelectedIndices.Count -lt 1 -and $result -eq [System.Windows.Forms.DialogResult]::OK)
{
Write-Warning 'Nothing was selected, please select a server.'
}
}
until (($result -eq [System.Windows.Forms.DialogResult]::OK -and $listBox.SelectedIndices.Count -ge 1) -or $result -ne [System.Windows.Forms.DialogResult]::OK)
if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
$Env -eq $listBox.SelectedItem
}
Here is a quickguid for using events in powershell:
Use | Get-Member -MemberType Event on your WinForms-object to get a list of available events > $OKButton | Get-Member -MemberType Event.
Add a scriptblock that shall be executed, once the event triggers. > $OKButton.add_Click({$ScriptGoesHere}). Watch out for the scope of the scriptblock (PS> help about_scopes)
I reworked your script with events instead of a loop and added comments at the important parts. Events are a lot easier to handle, if you have multiple things going on. But the scopes are kind of a drawback.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = "Select an environment"
$form.Size = New-Object System.Drawing.Size(380,250)
$form.StartPosition = "CenterScreen"
$form.Topmost = $True
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(10,180)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
#$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK #Moved to the event
$form.AcceptButton = $OKButton
$form.Controls.Add($OKButton)
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.Text = "Please select an environment:"
$form.Controls.Add($label)
$listBox = New-Object System.Windows.Forms.ListBox
$listBox.Location = New-Object System.Drawing.Point(10,40)
$listBox.Size = New-Object System.Drawing.Size(150,140) # the second number is the height
#$listBox.Height = 140
[void] $listBox.Items.AddRange(#("PROD", "QA1", "QA2", "TR"))
$form.Controls.Add($listBox)
$comboBox = New-Object System.Windows.Forms.ComboBox
$comboBox.Location = New-Object System.Drawing.Point(190,40)
$comboBox.Size = New-Object System.Drawing.Size(150,20)
[void] $comboBox.Items.AddRange(#("PROD", "QA1", "QA2", "TR"))
$form.Controls.Add($comboBox)
#region Events
#register some events that trigger actions
$listBox.add_SelectedIndexChanged({
#The event is actually $listBox.SelectedIndexChanged but we want to ADD an action. add_SelectedIndexChanged is not listed in get-member.
$ListSelected = $listBox.SelectedItem
Write-Host "ListSelected = $ListSelected"
# You will note that $ListSelected is not available outside the event yet. It is in the scope of the scriptblock.
})
$comboBox.add_SelectedIndexChanged({
#To prevent trouble with the scope, define the variables in a higher scope
$script:SelectedItem = $comboBox.SelectedItem
$global:SelectedIndex = $comboBox.SelectedItem
write-host "SelectedItem = $SelectedItem"
})
$script:OKScript = {
# or define the whole ScriptBlock in a higher scope...
if( $comboBox.SelectedIndex -ge 0){
$Env = $comboBox.SelectedItem #Be carefull $env is the beginning of environmental variables like $env:path
$form.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.close()
}else{
Write-Warning 'Nothing was selected, please select a server.'
}
}
# ...and DotSource it to execute it in the same scope:
$OKButton.add_Click({. $OKScript})
#endregion
$result = $form.ShowDialog()
<# This would work but we use events
if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
$SelectedItem = $comboBox.SelectedItem
$SelectedIndex = $comboBox.SelectedItem
}
#>
write-host "`r`nform is closed`r`nhere are the results:"
"ListSelected = $ListSelected"
"result = $result"
"env = $env"
"SelectedIndex = $SelectedIndex"
"SelectedItem = $SelectedItem"

Intermittent error (index into null) with detection of SelectedIndex change in Windows Forms

I'm getting an intermittent error with this method of changing a forms text label according to the selected item in a Listview box.
Example code as below, changing the entry will intermittently give:
Cannot index into a null array.
At C:\temp\test.ps1:62 char:5
+ $SelectedPath.Text = $VMsListBox.SelectedItems.SubItems[1].Text
# Import namespaces
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Demo'
$form.Size = '580,545'
$form.StartPosition = 'CenterScreen'
$form.FormBorderStyle = 'FixedSingle'
$form.MaximizeBox = $false
# Listview box to display found open files
$VMsListBox = New-Object System.Windows.Forms.ListView
$VMsListBox.View = [System.Windows.Forms.View]::Details
$VMsListBox.Location = '15,120'
$VMsListBox.size = '435,10'
$VMsListBox.Height = 250
$VMsListBox.Columns.Add('Name') | Out-Null
$VMsListBox.Columns.Add('Path') | Out-Null
$VMsListBox.FullRowSelect = $true
$VMsListBox.MultiSelect = $false
# Selected file label
$SelectedFnameLbl = New-Object System.Windows.Forms.Label
$SelectedFnameLbl.Location = '10,25'
$SelectedFnameLbl.Size = '80,19'
$SelectedFnameLbl.Text = 'File Name:'
# Selected file name
$SelectedFname = New-Object System.Windows.Forms.Label
$SelectedFname.Location = '100,25'
$SelectedFname.Size = '300,19'
$SelectedFname.Text = 'n/a'
$SelectedFname.AutoEllipsis = $true
# Path Label
$SelectedFileLbl = New-Object System.Windows.Forms.Label
$SelectedFileLbl.Location = '10,45'
$SelectedFileLbl.Size = '80,19'
$SelectedFileLbl.Text = 'File Path:'
# Selected filepath
$SelectedPath = New-Object System.Windows.Forms.Label
$SelectedPath.Location = '100,45'
$SelectedPath.Size = '300,19'
$SelectedPath.Text = 'n/a'
$SelectedPath.AutoEllipsis = $true
$form.Controls.AddRange(#($VMsListBox,$SelectedFileLbl,$SelectedPath,$SelectedFnameLbl,$SelectedFname))
# Populate ListView
$Files = Get-ChildItem -Path 'c:\temp' -File
$Files | ForEach-Object {
$Entry = New-Object System.Windows.Forms.ListViewItem($_.Name) -ErrorAction Stop
$Entry.SubItems.Add($_.FullName) | Out-Null
$VMsListBox.Items.Add($Entry) | Out-Null
}
$VMsListBox_SelectedIndexChanged={
$SelectedFname.Text = $VMsListBox.SelectedItems.Text
$SelectedPath.Text = $VMsListBox.SelectedItems.SubItems[1].Text
Write-Host "Entry changed"
}
$VMsListBox.Add_SelectedIndexChanged($VMsListBox_SelectedIndexChanged)
# Show form
$form.ShowDialog() | Out-Null
$form.Dispose()
Can anyone point me where I'm going wrong please? Or is there a better way of doing this
Ok, I found this
Apparently retained legacy behaviour and the fix is to check for null:
$VMsListBox_SelectedIndexChanged={
If($VMsListbox.SelectedItems -ne $Null){
$SelectedFname.Text = $VMsListBox.SelectedItems.Text
$SelectedPath.Text = $VMsListBox.SelectedItems.SubItems[1].Text
Write-Host "Entry changed"
}
}

Get returned variable from add_Click event (PowerShell)

The goal is to get the "The Returned Variable Worked" into a variable.
I am trying to avoid the using any globals such as $Script: $Global:
Out-host is not an option. Ideally the "Generate-Form" function could return the variable.
Function Button_Click()
{
[System.Windows.Forms.MessageBox]::Show("Button Clicked")
$returnedVariable = "The Returned Variable Worked"
Return $returnedVariable
}
Function Generate-Form {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Build Form
$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
# Add Button
$Button = New-Object System.Windows.Forms.Button
$Button.Location = New-Object System.Drawing.Size(35,35)
$Button.Size = New-Object System.Drawing.Size(120,23)
$Button.Text = "Show Dialog Box"
$Form.Controls.Add($Button)
#Add Button event
$OutputVariableFromGenerateForm = $Button.Add_Click({$returnedVar = Button_Click ; $Form.Close(); return $returnedVar})
# $returnedVar contains an array #("OK,"The Returned Variable Worked"),
# but it appears to be out of scope because it is in a script block.
# I only want "The Returned Variable Worked"
#Show the Form
$form.ShowDialog()| Out-Null
$OutputVariableFromGenerateForm # This is null
$returnedVar # This is null
} #End Function
Generate-Form
Use a hashtable:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$hash = [hashtable]::Synchronized(#{})
$hash.returnedVar = ""
Function Button_Click() {
[System.Windows.Forms.MessageBox]::Show("Button Clicked")
$returnedVariable = "The Returned Variable Worked"
Return $returnedVariable
}
Function Generate-Form {
# Build Form
$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
# Add Button
$Button = New-Object System.Windows.Forms.Button
$Button.Location = New-Object System.Drawing.Size(35,35)
$Button.Size = New-Object System.Drawing.Size(120,23)
$Button.Text = "Show Dialog Box"
$Form.Controls.Add($Button)
#Add Button event
$OutputVariableFromGenerateForm = $Button.Add_Click({$hash.returnedVar = Button_Click; [void]$form.Close(); [void]$form.Dispose(); })
# $returnedVar contains an array #("OK,"The Returned Variable Worked"),
# but it appears to be out of scope because it is in a script block.
# I only want "The Returned Variable Worked"
#Show the Form
[void]$form.ShowDialog()
$OutputVariableFromGenerateForm # This is OK
$hash.returnedVar # This is "The Returned Variable Worked"
} #End Function
Generate-Form

Display custom message while installation using powershell

i am unable to figure out the mistake that i am doing.The following script will call a bunch of batch files and while the batch files are doing there job, the progress will be shown in a progress bar along with the name of the script. What i want to achieve is to display a custom message during the installation process. I am not able to find out the mistake, any help is deeply appreciated.
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
Set-Location $PSScriptRoot
#Call scripts for installation
$ScriptsHome = Get-Item '.\test\forPS\*'
#Define Form
# Init Form
$Form = New-Object System.Windows.Forms.Form
$Form.width = 1000
$Form.height = 200
$Form.Text = "** Installation in Progress-PLEASE DO NOT CLOSE THIS WINDOW**"
$Form.Font = New-Object System.Drawing.Font("Times New Roman" ,12, [System.Drawing.FontStyle]::Regular)
$Form.MinimizeBox = $False
$Form.MaximizeBox = $False
$Form.WindowState = "Normal"
$Form.StartPosition = "CenterScreen"
$Form.Opacity = .8
$Form.BackColor = "Gray"
#Define ICON for Form
$Icon = New-Object System.Drawing.Graphics (".\ICON.jpg")
$Form.Icon = $Icon
# Init ProgressBar
$ProgressBar = New-Object System.Windows.Forms.ProgressBar
$ProgressBar.Maximum = $ScriptsHome.Count
$ProgressBar.Minimum = 0
$ProgressBar.Location = new-object System.Drawing.Size(10,70)
$ProgressBar.size = new-object System.Drawing.Size(967,10)
$Form.Controls.Add($ProgressBar)
$Form.Controls.Add($Messages)
#Running Script Name
$Label = New-Object System.Windows.Forms.Label
$Label.AutoSize = $true
$Label.Location = New-Object System.Drawing.Point(10,50)
$Form.Controls.Add($Label)
#Define Array messages
#Array
$Messages = #("Preparing to install patch set..Preparing to stop all related processes",
"Upgrading the application",
"Copying the missing folder",
"Applying the patch",
"Starting all previously stopped Services",
"Checkcing healthyness of the system after the installation this is may take up to half an hour...",
"Rebooting Server"
)
$Messages = New-Object System.Windows.Forms.Label
$Messages.AutoSize = $true
$Messages.Location = New-Object System.Drawing.Point(10,50)
$Form.Controls.Add($Messages)
# Add_Shown action
$ShownFormAction = {
$Form.Activate()
foreach ($script in $ScriptsHome) {
$ProgressBar.Increment(1)
#$Messages.Text = $Messages[$Messages]
$Label.Text = "$($script.Name)"
Start-Process $script.FullName -Wait -WindowStyle Hidden
}
$Form.Dispose()
}
$Form.Add_Shown($ShownFormAction)
# Show Form
$Form.ShowDialog()
Thanks in advance.
You're reusing the same variable name for the list of messages and the label showing the message itself:
$Messages = #("Preparing to install patch set..Preparing to stop all related processes",
"Upgrading the application",
"Copying the missing folder",
"Applying the patch",
"Starting all previously stopped Services",
"Checkcing healthyness of the system after the installation this is may take up to half an hour...",
"Rebooting Server"
)
$Messages = New-Object System.Windows.Forms.Label
$Messages.AutoSize = $true
$Messages.Location = New-Object System.Drawing.Point(10,50)
Rename one of them (ie. use $MessageLabel for the label):
$MessageLabel = New-Object System.Windows.Forms.Label
$MessageLabel.AutoSize = $true
$MessageLabel.Location = New-Object System.Drawing.Point(10,50)
Since you increment the ProgressBar by 1 every step, you can reuse the progress bar value to index into the $Messages array:
foreach ($script in $ScriptsHome) {
$ProgressBar.Increment(1)
$MessageLabel.Text = $Messages[$ProgressBar.Value - 1]
$Label.Text = "$($script.Name)"
Start-Process $script.FullName -Wait -WindowStyle Hidden
}

Resources