PowerShell Windows Forms Wrapper - forms

In PowerShell it is quiet common to use Windows Forms to build a User Interface for small cmdlets but the syntaxis required for this are often partly redundant and quiet verbose. This leads to the question:
Is there a way to minimize the code required or does there exist a Windows Forms wrapper for PowerShell to reduce the verbose and redundant syntaxis?
I am not looking for the ShowUI as this solution is too heavy considering it based on Windows Presentation Foundation (see also: WPF vs WinForms) and the fact that it concerns a PowerShell module which makes it more difficult to deploy it than a wrapper function.

In a lot of cases a wrapper in not required to make your code less verbose, take e.g. the lengthy WinForms PowerShell script here. Code pieces like this:
$System_Windows_Forms_Padding = New-Object System.Windows.Forms.Padding
$System_Windows_Forms_Padding.All = 3
$System_Windows_Forms_Padding.Bottom = 3
$System_Windows_Forms_Padding.Left = 3
$System_Windows_Forms_Padding.Right = 3
$System_Windows_Forms_Padding.Top = 3
$Tab1.Padding = $System_Windows_Forms_Padding
Can easily be simplified in WinForms to a single line:
$Tab1.Padding = 3
And if the padding would be different for each side, PowerShell will automatically convert:
$Tab1.Padding = "4, 6, 4, 6"
Note: PowerShell does not convert $Tab1.Padding = "3" or $Tab1.Padding = "4, 6"
Nevertheless, the native way to create a windows form control is far from DRY (don't repeat yourself) programming. Although (multiple) properties can be added at creation (using:New-Object System.Windows.Forms.Button -Property #{Location = "75, 120"; Size = "75, 23"}) , multiple properties can't be set right away at a later state. Above that, it isn't quick and easy to add events1, child controls and container properties (as e.g. RowSpan), or any combination, intermediately at creation of a windows form control. Bottom line, you have to reference the windows form control over and over again to set its properties and more (with e.g. $OKButton.<property> = ... as in this example) :
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(75,120)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
That's why I have created a reusable PowerShell Form Control wrapper that let's you minimize Windows Forms (WinForms) code to it's essence.
1) unless you use On<event> methods, see also: addEventListener vs onclick
PowerShell Form-Control Wrapper
Function Form-Control {
[CmdletBinding(DefaultParametersetName='Self')]param(
[Parameter(Position = 0)]$Control = "Form",
[Parameter(Position = 1)][HashTable]$Member = #{},
[Parameter(ParameterSetName = 'AttachChild', Mandatory = $false)][Windows.Forms.Control[]]$Add = #(),
[Parameter(ParameterSetName = 'AttachParent', Mandatory = $false)][HashTable]$Set = #{},
[Parameter(ParameterSetName = 'AttachParent', Mandatory = $false)][Alias("Parent")][Switch]$GetParent,
[Parameter(ParameterSetName = 'AttachParent', Mandatory = $true, ValueFromPipeline = $true)][Windows.Forms.Control]$Container
)
If ($Control -isnot [Windows.Forms.Control]) {Try {$Control = New-Object Windows.Forms.$Control} Catch {$PSCmdlet.WriteError($_)}}
$Styles = #{RowStyles = "RowStyle"; ColumnStyles = "ColumnStyle"}
ForEach ($Key in $Member.Keys) {
If ($Style = $Styles.$Key) {[Void]$Control.$Key.Clear()
For ($i = 0; $i -lt $Member.$Key.Length; $i++) {[Void]$Control.$Key.Add((New-Object Windows.Forms.$Style($Member.$Key[$i])))}
} Else {
Switch (($Control | Get-Member $Key).MemberType) {
"Property" {$Control.$Key = $Member.$Key}
"Method" {Invoke-Expression "[Void](`$Control.$Key($($Member.$Key)))"}
"Event" {Invoke-Expression "`$Control.Add_$Key(`$Member.`$Key)"}
Default {Write-Error("The $($Control.GetType().Name) control doesn't have a '$Key' member.")}
}
}
}
$Add | ForEach {$Control.Controls.Add($_)}
If ($Container) {$Container.Controls.Add($Control)}
If ($Set) {$Set.Keys | ForEach {Invoke-Expression "`$Container.Set$_(`$Control, `$Set.`$_)"}}
If ($GetParent) {$Container} Else {$Control}
}; Set-Alias Form Form-Control
Syntax
Creating a control
<System.Windows.Forms.Control> = Form-Control [-Control <String>] [-Member <HashTable>]
Modifying a control
<Void> = Form-Control [-Control <System.Windows.Forms.Control>] [-Member <HashTable>]
Adding a (new) control to a container
<System.Windows.Forms.Control> = Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Add <System.Windows.Forms.Control[]>]
Piping a container to a (new) control
<System.Windows.Forms.Control> = <System.Windows.Forms.Control> | Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Set <HashTable>] [-PassParent]
Parameters
-Control <String>|<System.Windows.Forms.Control> (position 0, default: Form)
The -Control parameter accepts either a Windows form control type name ([String]) or an existing form control ([System.Windows.Forms.Control] ). Windows form control type names are like Form, Label, TextBox, Button, Panel, ..., etc.
If a Windows form control type name ([String]) is supplied, the wrapper will create and return a new Windows form control with properties and settings as defined by the rest of the parameters.
If an existing Windows form control ([System.Windows.Forms.Control] ) is supplied, the wrapper will update the existing Windows form control using the properties and settings as defined by the rest of the parameters.
-Member <HashTable> (position 1)
Sets property values, invokes methods and add events on a new or existing object.
If the hash name represents property on the control, e.g. Size = "50, 50", the value will be assigned to the control property value.
If the hash name represents method on the control, e.g. Scale = {1.5, 1.5}, the control method will be invoked using the value for arguments .
If the hash name represents event on the control, take e.g. Click = {$Form.Close()}, the value ( [ScriptBlock]) will be added to the control events.
Two collection properties, ColumnStyles and RowStyles, are simplified especially for the TableLayoutPanel control which is considered a general substitute for the WPF Grid control:
- The ColumnStyles property, clears all column widths and reset them with the ColumnStyle array supplied by the hash value.
- The RowStyles property, clears all row Heigths and reset them with the RowStyle array supplied by the hash value.
Note: If want to add or insert a single specific ColumnStyle or RowStyle item, you need to fallback on the native statement, as e.g.: [Void]$Control.Control.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle("Percent", 100)).
-Add <Array>
The -Addparameter adds one or more child controls to the current control.
Note: the -add parameter cannot be used if container is piped to the control.
-Container <System.Windows.Forms.Control> (from pipeline)
The parent container is usually provided from the pipeline: $ParentContainer | Form $ChildControl and attached a (new) child control to the concerned container.
-Set <HashTable>
The -Setparameter sets (SetCellPosition, SetColumn, SetColumnSpan, SetRow, SetRowSpan and SetStyle) the specific child control properties related its parent panel container, e.g. .Set RowSpan = 2
Note: the -set column - and row parameters can only be used if a container is piped to the control.
-GetParent
By default the (child) control will be returned by the form-control function unless the -GetParent switch is supplied which will return the parent container instead.
Note: the -set column - and row parameters can only be used if a container is piped to the control.
Examples
There are two way to setup the Windows Forms hierarchy:
Adding a (new) control to a container
Piping a container to a (new) control
Adding a (new) control to a container
For this example I have reworked the Creating a Custom Input Box at learn.microsoft.com using the PowerShell Form-Control wrapper:
$TextBox = Form TextBox #{Location = "10, 40"; Size = "260, 20"}
$OKButton = Form Button #{Location = "75, 120"; Size = "75, 23"; Text = "OK"; DialogResult = "OK"}
$CancelButton = Form Button #{Location = "150, 120"; Size = "75, 23"; Text = "Cancel"; DialogResult = "Cancel"}
$Result = (Form-Control Form #{
Size = "300, 200"
Text = "Data Entry Form"
StartPosition = "CenterScreen"
KeyPreview = $True
Topmost = $True
AcceptButton = $OKButton
CancelButton = $CancelButton
} -Add (
(Form Label #{Text = "Please enter the information below:"; Location = "10, 20"; Size = "280, 20"}),
$TextBox, $OKButton, $CancelButton
)
).ShowDialog()
if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
$x = $TextBox.Text
$x
}
Note 1: Although the adding controls appears more structured especially for small forms, the drawback is that can't invoke methods that relate to both the parent container and child control (like -Set RowSpan).
Note 2: You might easily get lost in open and close parenthesis if try build child (or even grandchild) controls directly in a parent container (like the above Label control). Besides it more difficult to reference such a child (e.g. $OKButton vs. $Form.Controls["OKButton"], presuming you have set the button property Name = "OKButton)
Piping a container to a (new) control
For this example, I have created a user interface to test the dockproperty behavior. The form looks like this:
The PowerShell Form-Control code required for this:
$Form = Form-Control Form #{Text = "Dock test"; StartPosition = "CenterScreen"; Padding = 4; Activated = {$Dock[0].Select()}}
$Table = $Form | Form TableLayoutPanel #{RowCount = 2; ColumnCount = 2; ColumnStyles = ("Percent", 50), "AutoSize"; Dock = "Fill"}
$Panel = $Table | Form Panel #{Dock = "Fill"; BorderStyle = "FixedSingle"; BackColor = "Teal"} -Set #{RowSpan = 2}
$Button = $Panel | Form Button #{Location = "50, 50"; Size = "50, 50"; BackColor = "Silver"; Enabled = $False}
$Group = $Table | Form GroupBox #{Text = "Dock"; AutoSize = $True}
$Flow = $Group | Form FlowLayoutPanel #{AutoSize = $True; FlowDirection = "TopDown"; Dock = "Fill"; Padding = 4}
$Dock = "None", "Top", "Left", "Bottom", "Right", "Fill" | ForEach {
$Flow | Form RadioButton #{Text = $_; AutoSize = $True; Click = {$Button.Dock = $This.Text}}
}
$Close = $Table | Form Button #{Text = "Close"; Dock = "Bottom"; Click = {$Form.Close()}}
$Form.ShowDialog()

Related

Set ContextMenu items from a string list

Basically I'm trying to create a ContextMenu that shows a list of mounted network drive, so the user can click on one and access it.
The problem is : the list is not fix, but depend on a string list extracted from a DataGrid.
In my mind, I could just create the menu items by looping on that string list.
Here is the code sample :
$Main_Tool_Icon = New-Object System.Windows.Forms
[...]
# Add menu entries
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
foreach ($nameTag in $dataGrid[$DATAGRID_COLUMN_NAME]){
$menuEntry = New-Object System.Windows.Forms.MenuItem
$menuEntry.Text = $nameTag
$Main_Tool_Icon.ContextMenu.MenuItems.AddRange($menuEntry)
}
# Add separator
$Main_Tool_Icon.ContextMenu.MenuItems.Add('-')
# Last one to show main form
$displayMainform = New-Object System.Windows.Forms.MenuItem
$displayMainform.Text = "Edit"
$Main_Tool_Icon.ContextMenu.MenuItems.Add($displayMainform)
It appears that's wrong and only shows the separator and last "Edit" line.
Is there a way to populate a contextual menu with a list ?
Or I should look for a better way to do the job ?
EDIT #Mathias R. Jessen
You're right about AddRange(), it's just a mistake.
I can't show everything since it's quite big. I'll try to synthesize an answer properly.
The data grid is created/populated by my script and is just a table of strings from a csv file.
$DATAGRID_COLUMN_NAME = 'NAME' it's one of constants defined for my datagrid headers.
Here is the setup
# set datagrid
$dataGrid = New-Object System.Windows.Forms.DataGridView
[...]
$dataGrid.Columns.Add($DATAGRID_COLUMN_NAME,'Name')
$dataGrid.Columns[0].DataPropertyName = $dataGrid.Columns[0].Name
Then some databinding
# initiate the data table
$table = New-Object System.Data.DataTable
[...]loop on csv file[...]
# bind data table to data grid
$dataGrid.DataSource = $table
EDIT #SantiagoSquarzon
You were right for pointing out $dataGrid[$DATAGRID_COLUMN_NAME]. The syntax was bad and returned something null. Instead, I looped over the $dataGrid.Rows like so:
foreach ($row in $dataGrid.Rows){
$nameTag = $row.Cells[$DATAGRID_COLUMN_NAME].Value
if ($nameTag -ne $null){
$menuEntry = New-Object System.Windows.Forms.MenuItem
$menuEntry.Text = $nameTag
$Main_Tool_Icon.ContextMenu.MenuItems.AddRange($menuEntry)
}
}
Now I just have to figure out how to invoke there click event.

Why isn't this form saving values to the $x and $y variables? [duplicate]

This question already has answers here:
Using form to return variable fails to produce a return value
(3 answers)
Closed 1 year ago.
I have a winform application which populate some data after I click on $button_UpdateTS, how do I add the data stored in a variable, that comes available after I click on that button ?
The data I want in my list view is stored in an array called $results
$button_UpdateTS = New-Object System.Windows.Forms.Button
$button_UpdateTS.Location = New-Object System.Drawing.Size(15, 954)
$button_UpdateTS.Size = New-Object System.Drawing.Size(320, 32)
$button_UpdateTS.TextAlign = "MiddleCenter"
$button_UpdateTS.Text = “Update Tasksequence”
$button_UpdateTS.Add_Click( { $Results = Set-DynamicVariables
-Manufacturer "$($listview_Vendor.SelectedItems)"
-TSPackageID "$($ListView_Tasksequences.SelectedItems.SubItems[1].Text)" -WhatIf })
$Form.Controls.Add($button_UpdateTS)
Which gives me :
$Results =
SKUNotExistsDriverName : XPS Notebook 9560
SKUNotExistsDriverID : PS10053F
SKUNotExistsDriverSKU : 07BE
SKUNotExistsDriverVersion : A12
SKUNotExistsBIOSName : XPS Notebook 9560
SKUNotExistsBIOSID : PS10053E
SKUNotExistsBIOSSKU : 07BE
SKUNotExistsBIOSVersion : 1.15.0
This is the list I want it stored in :
$Global:listview_NotExists_SKU = New-Object System.Windows.Forms.ListView
$listview_NotExists_SKU.Location = New-Object System.Drawing.Size(515, 670)
$listview_NotExists_SKU.Size = New-Object System.Drawing.Size(486, 235)
$listview_NotExists_SKU.View = "Details"
$listview_NotExists_SKU.FullRowSelect = $true
$listview_NotExists_SKU.MultiSelect = $true
$listview_NotExists_SKU.Sorting = "None"
$listview_NotExists_SKU.AllowColumnReorder = $true
$listview_NotExists_SKU.GridLines = $true
$listview_NotExists_SKU.Add_ColumnClick( { SortListView $this $_.Column })
$Form.Controls.Add($listview_NotExists_SKU)
I tried with this function, but that does not work:
Function Get-Results {
ForEach ($Result in $Results) {
$listview_NotExists_SKU.Items.Add($Result)
}
}
$Form.Add_Shown( { $Form.Load; Get-results })
Because an event-handling script block added with e.g. .Add_Click() runs in in a child scope of the caller, assigning to variable $Results there ($Results = ...) creates a scope-local variable that neither the scope in which the event handler was set up nor subsequently invoked event handlers can see.
To create a variable in the script scope, which subsequently invoked event handlers can see as well[1], use the $script: scope specifier:
$button_UpdateTS.Add_Click( { $script:Results = ... } )
Note:
If the scope in which the event handlers are set up isn't the script scope (e.g., if the code is inside a function) and you want to more generically reference that scope from within an event handler, use Set-Variable -Scope 1 -Name Results -Value ..., which targets the respective parent scope.[1]
An alternative to setting a variable in the parent scope explicitly is to use a hashtable defined in the parent scope whose entries can be used in lieu of variables that the event-handler script blocks can modify too.[2] See this answer for an example.
[1] For more information about scopes in PowerShell, see the bottom section of this answer.
[2] This technique works, because even though the variable containing the hashtable is defined in the parent scope, the child scope can access its value and modify the entries of the referenced hashtable object rather than the variable itself.

Assigning contentcontrol.range.text is not working

I try to fill some word contentcontrols (Plain Text) from Excel using Powershell, but they are set to empty / display the placeholder text.
My code:
ForEach($Control in $Doc.ContentControls)
{
write-host $control.Title
[string]$FieldName = $control.Title.Trim()
[string]$FieldContent = $Workbook.Names($FieldName).RefersToRange.Value()
write-host $FieldContent
$control.LockContents = $False
$control.LockContentControl = $False
$control.Range.Text = "Test"
$control.Range.Text = $FieldContent
write-host $FieldContent
}
When I assign 'Test' it works. So the control is editable and accepts the text-property.
When I assign $FieldContent the ContentControl gets emptied and I see the default text 'Click to enter text'.
$FieldContent is filled. $FieldContent as well as $control.Range.Text have the type System.Object.String.
No idea what is wrong
Thanks for any hints.

How to replace/update the Value of an Attribute in LDAP Directory using PowerShell?

The entries in our companys Non-AD LDAP Server look like this:
uid = e145871
sn = Smith
givenName = John
department = Research & Development
department = Human Resource
And so on...
I've developed a PowerShell script to add specific attributes and values which is working just fine. Now I need to replace specific values but the issue is the identical attribute name. (In this case it's "department")
My goal is to replace "Research & Development" with "Something Else". If I run the following script it gets replaced but Human Resource is deleted as well. Is it possible to replace only one value without touching/deleting the other?
$r = New-Object -TypeName System.DirectoryServices.Protocols.ModifyRequest
$r.DistinguishedName = "uid=e145871,ou=identities,ou=users,o=items,dc=company,dc=domain,dc=com"
$DirectoryRequest_value = New-Object "System.DirectoryServices.Protocols.DirectoryAttributeModification"
$DirectoryRequest_value.Name = "department"
$DirectoryRequest_value.Contains("Research & Development")
$DirectoryRequest_value.Operation = [System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Replace
$DirectoryRequest_value.Add("SomethingElse")
$r.Modifications.Add($DirectoryRequest_value)
$result = $connection.SendRequest($r)
Thanks!
The LDAP Replace operation replaces (or overwrites) the entire value of the attribute, including any existing values that might exist as part of a multi-valued attribute.
From RFC4511 §4.6 - "Modify Operation":
- operation: Used to specify the type of modification being
performed. Each operation type acts on the following
modification. The values of this field have the following
semantics, respectively:
[...]
replace: replace all existing values of the modification
attribute with the new values listed, creating the attribute
if it did not already exist. A replace with no value will
delete the entire attribute if it exists, and it is ignored
if the attribute does not exist.
Instead, add two separate modifications to the request - one to add "SomethingElse" and one to remove "Research & Development":
$targetObject = 'uid=e145871,ou=identities,ou=users,o=items,dc=company,dc=domain,dc=com'
$attributeName = 'department'
$oldValue = 'Research & Development'
$newValue = 'SomethingElse'
$request = [System.DirectoryServices.Protocols.ModifyRequest]::new()
$request.DistinguishedName = $targetObject
# This modification will add the new value "SomethingElse"
$addNewDepartment = #{
Name = $attributeName
Operation = 'Add'
} -as [System.DirectoryServices.Protocols.DirectoryAttributeModification]
$addNewDepartment.Add($newValue) |Out-Null
$request.Modifications.Add($addNewDepartment) |Out-Null
# This modification will remove the old value "Research & Development"
$removeOldDepartment = #{
Name = $attributeName
Operation = 'Delete'
} -as [System.DirectoryServices.Protocols.DirectoryAttributeModification]
$removeOldDepartment.Add($oldValue) |Out-Null
$request.Modifications.Add($removeOldDepartment) |Out-Null
$result = $connection.SendRequest($request)

PowerShell NotifyIcon Context Menu

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:
Run a function when there is a left click event on the icon
Add another option to the right click menu that also runs a function
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)
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.