Powershell multiple dynamic buttons - powershell

I built a dynamic Powershell GUI but i have trouble getting my buttons to work correctly.
I created a function to add texboxes and buttons. However they do not correspond correctly at this point. because i'm not sure how to bind the add_click to a specific textbox.
Here is the example code:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$Form = New-Object System.Windows.Forms.Form
$Form.Size = New-Object System.Drawing.Size(300,300)
$ButtonAdd = New-Object System.Windows.Forms.Button
$ButtonAdd.Location = New-Object System.Drawing.Point(20,20)
$ButtonAdd.Size = New-Object System.Drawing.Size(50,30)
$ButtonAdd.Text = 'Add'
$Form.Controls.Add($ButtonAdd)
$global:Counter = 0
$global:ButtonPosY = 20
$global:DefaultTextValue = ""
$ButtonAdd.Add_Click{Add-Fields}
Function Create-Button {
param ( $ButtonPosY, $TextBoxPosY)
$TextBoxField = New-Object System.Windows.Forms.TextBox
$TextBoxField.Location = New-Object System.Drawing.Point(181,$TextBoxPosY)
$TextBoxField.Size = New-Object System.Drawing.Size(50,30)
$TextBoxField.Text = $global:DefaultTextValue
New-Variable -name TextBoxField$global:Counter -Value $TextBoxField -Scope Global -Force
$Form.controls.Add($((Get-Variable -name TextBoxField$global:Counter).value))
$ButtonClick = New-Object System.Windows.Forms.Button
$ButtonClick.Location = New-Object System.Drawing.Point(100,$global:ButtonPosY)
$ButtonClick.Size = New-Object System.Drawing.Size(50,30)
$ButtonClick.Text = 'Click'
New-Variable -name ButtonClick$global:Counter -Value $ButtonClick -Scope Global -Force
$Form.controls.Add($((Get-Variable -name ButtonClick$global:Counter).value))
$ButtonClick.Add_Click({
$((Get-Variable -name TextBoxField$global:Counter -Scope Global).value).Text = 'hello'
})
}
Function Add-Fields {
$global:Counter = $global:Counter + 1
$global:ButtonPosY = $global:ButtonPosY + 40
Create-Button -TextBoxPosY $global:ButtonPosY -ButtonPosY $global:ButtonPosY
}
Create-Button -TextBoxPosY 21 -ButtonPosY 20
$Form.ShowDialog()
If the click button is pressed each time after new input is added everything works fine. however if multiple input fields are added first and then the click button is pressed the code breaks.
The Problem is here:
$ButtonClick.Add_Click({...})
I don't know how to add the counter (in my case $global:Counter) to the $ButtonClick variable as in $ButtonClick0, $ButtonClick1, ... etc. So right now when i add more buttons by calling the function the input will always be applied to the last added textbox since the add_click is not linked to a individual $ButtonClick0 variable.
How would this be done right?

After some serious reading i figured it out.
A name needs to be added to $ButtonClick
$ButtonClick.Name = $global:Counter
And the Add_Click event should look the following:
$ButtonClick.Add_Click({
[System.Object]$Sender = $args[0]
$((Get-Variable -name ('TextBoxField' + [int]$Sender.Name)).value).Text = 'hello'
})
However i don't know if this is how it should be done. Please post your solution if it looks different.

Related

Random number generator not working after converting to Exe

I have written a dice roller script. I have one with a single die and one with two dice. they both work in powershell, and the single die version works after converting to .exe using ps2exe. but the two die version runs as an exe but I get the following error "You cannot call a method on a null-valued expression"
Below is the 2 die script that gives the error after converting.
<#
.NAME
Random Dice Roller
#>
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$Display1 = 1,2,3,4,5,6 | Get-Random
$Display2 = 1,2,3,4,5,6 | Get-Random
$LabelImage = [system.drawing.image]::FromFile("f:\psscripts\Die1.png")
$Form = New-Object system.Windows.Forms.Form
$Form.ClientSize = New-Object System.Drawing.Point(400,300)
$Form.text = "Roll The Dice"
$Form.TopMost = $false
$Form.location = New-Object System.Drawing.Point(1,1)
$Form.StartPosition = "CenterScreen"
$Die1 = New-Object system.Windows.Forms.Label
$Die1.Text = "$Display1"
$Die1.AutoSize = $false
$Die1.width = 200
$Die1.height = 200
$Die1.location = New-Object System.Drawing.Point(1,1)
$Die1.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',150,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
$Die1.ForeColor = [System.Drawing.ColorTranslator]::FromHtml("#000000")
$Die1.BackgroundImage = $LabelImage
$Die2 = New-Object system.Windows.Forms.Label
$Die2.Text = "$Display2"
$Die2.AutoSize = $false
$Die2.width = 200
$Die2.height = 200
$Die2.location = New-Object System.Drawing.Point(200,1)
$Die2.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',150,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
$Die2.ForeColor = [System.Drawing.ColorTranslator]::FromHtml("#000000")
$Die2.BackgroundImage = $LabelImage
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size(175,220)
$Button1.Size = New-Object System.Drawing.Size(80,50)
$Button1.Text = "Roll Me"
$Button1.Add_Click({Roll})
$Form.controls.AddRange(#($Die1,$Die2,$Button1))
$Die.Add_Click({ Roll })
#region Logic
function Roll{
$Form.Close()
$Form.Dispose()
.\Dice.exe}
#endregion
[void]$Form.ShowDialog()
There are two possible causes here:
Note that there is no safety in wrapping you PowerShell script in an .exe file. In fact the PowerShell script is extracted in a temporary folder and executed from there which might be the cause of your issue (e.g. the relative .\Dice.exe location)
It is unwise to do a $Form.Dispose() from within a form function/event
Remove that from the function and put it outside the form, e.g.:
[void]$Form.ShowDialog(); $Form.Dispose()
(Or do not use the Dispose method at all as PowerShell will usually already takes care of that if you [void] the $From.)
What is your motive for creating an EXE?
An alternative might be to create a CMD file with a Batch bootstrap instead. Create a CMD file with the following code and include your script after this.
<# :
#ECHO OFF
SET f0=%~f0
PowerShell -NoProfile -ExecutionPolicy RemoteSigned -Command ".([scriptblock]::Create((get-content -raw $Env:f0)))"
PAUSE
GOTO :EOF
<#~#>
# Insert PowerShell script after this line.

Text parameter not working with Powershell Progress Bar

I've been looking around for the past half hour on how to show the text inside of a GUI ProgressBar in Powershell, and everything I've tried has failed. I've even been referencing MSoft docs on it.
Am I doing something wrong? How do I add in the text?
This isn't my full script or exactly how I'll be using it - I just made an example so I could try to get it working.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Size = '500,300'
$form.StartPosition = 'CenterScreen'
$form.Topmost = $true
$computerList = 'server01', 'server02', 'server03', 'server04', 'server05', 'server06', 'server07', 'server08', 'server09', 'server10'
$progressbar1 = New-Object System.Windows.Forms.ProgressBar
$progressbar1.Size = '300, 20'
$progressbar1.Location = '20,60'
$progressbar1.Text = "Processing..."
$progressbar1.Maximum = $computerList.Count
$progressbar1.Step = 1
$progressbar1.Value = 0
foreach ($computer in $computerList){
$progressbar1.PerformStep()
}
$form.Controls.Add($progressbar1)
$form.ShowDialog()
I guess the easiest way is to have a Label control above the progressbar and update the text in there:
$progressLabel = New-Object System.Windows.Forms.Label
$progressLabel.Size = '300, 20'
$progressLabel.Location = '20,40'
$progressLabel.Text = "Processing..."
$form.Controls.Add($progressLabel)
foreach ($computer in $computerList){
$progressLabel.Text = "Doing stuff on computer '$computer'.."
$progressbar1.PerformStep()
# perform your action on $computer here
}

object $Button within foreach / how to reference it to change color

I'm building GUI using foreach,
Now... could do 16 buttons but trying to automate build a little:
1,2,3,4 | foreach { $Button...}
all this works and I get 16 buttons with correct offsets but got stuck a little when decided to use jobs within buttons, background job pings, sleeps, pings - if fail to ping job ends and at this point I want it to change button's color
Since all buttons are "$Button",
can foreach generate $button with different number at the end? ie $Button1, $Button2... etc ?
tried $Button$_ but this didn't work
or any thoughts on how to reference buttons so depending on which job finished could change color of that button?
Cheers
#Wasif_Hasan this didn't work
$i=120
1..4 | Foreach {Set-Variable -Name "Button$($_)" -Value "Value"
$i=$i+70
$Button = New-Object System.Windows.Forms.Button
$Button.Location = New-Object System.Drawing.Point(150,$i)
$Button.Size = New-Object System.Drawing.Size(75,23)
$Button.Text = 'Cancel'+$_
$Button.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $Button
$form.Controls.Add($Button)
}
$Button3.Text = 'ok'+$_
Error:
The property 'Text' cannot be found on this object. Verify that the property exists and can be set.
Code that can be saved as .PS1 to check...
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Select a Computer'
$form.Size = New-Object System.Drawing.Size(300,400)
$form.StartPosition = 'CenterScreen'
$i=10
1..4 | Foreach {Set-Variable -Name "Button$($_)" -Value "Value"
$i=$i+50
$Button = New-Object System.Windows.Forms.Button
$Button.Location = New-Object System.Drawing.Point(150,$i)
$Button.Size = New-Object System.Drawing.Size(75,23)
$Button.Text = "Number"+$_
$form.Controls.Add($Button)
}
$Button3.Text = "Test"
$result = $form.ShowDialog()
Error:
The property 'Text' cannot be found on this object. Verify that the property
exists and can be set.
At line:19 char:1
+ $Button3.Text = "Test"
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException
Solution, Thanks Wasif_Hasan!
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Select a Computer'
$form.Size = New-Object System.Drawing.Size(300,400)
$form.StartPosition = 'CenterScreen'
$Button=1,2,3,4
$i=10
$k=1
1,2,3,4 | Foreach {
$i=$i+50
New-Variable "Button$_" -Value $(new-object System.Windows.Forms.Button -Property #{
Name = 'Dvar'
Location = New-Object System.Drawing.Size(10,$i)
Size = New-Object System.Drawing.Size(75,23)
Text = $_
})
$form.Controls.Add($(Get-Variable "Button$_" -ValueOnly))
$k=$k++
}
$Button2.Text="Hello"
$result = $form.ShowDialog()
$k=5
Remove-Variable Button*
$form.ActiveControl.Text
Result:
1
Hello
2
3
Yes, to set them use this:
1..4 | Foreach {Set-Variable -Name "Button$($_)" -Value "Value"}
And you can also get the values like:
1..4 | Foreach {(Get-Variable -Name "Button$($_)").Value}
All will be created like $Button1,$Button2 etc.

The correctness of creating objects using a loop

Is such a creation of objects in a loop correct? I do it this way and it works. But maybe I'm wrong and there is a more literate way. Thank you.
0..10 | ForEach-Object {
New-Variable "Button$_" -EA 0
(Get-Variable "Button$_").Value = New-Object System.Windows.Forms.Button
$Button = (Get-Variable "Button$_").Value
$Button.FlatStyle = 'Flat'
...
}
Editing. I mean the correctness of this part:
New-Variable "Button$_" -EA 0
(Get-Variable "Button$_").Value = New-Object System.Windows.Forms.Button
Assuming you really want to create distinct variables $Button1, $Button2, ... rather than storing your button objects in a single array variable:
New-Variable "Button$_" -EA 0
(Get-Variable "Button$_").Value = New-Object System.Windows.Forms.Button
works, but can more simply and more efficiently be written as:
Set-Variable "Button$_" (New-Object System.Windows.Forms.Button)
Note: The "..." around Button$_ isn't strictly necessary here, but it makes the intent clearer.
or, in PSv5+:
Set-Variable "Button$_" ([System.Windows.Forms.Button]::new())
If you want to obtain a reference to the newly created variable object at the same time, using -PassThru:
$buttonVar = Set-Variable -PassThru "Button$_" ([System.Windows.Forms.Button]::new())
$buttonVar.Value.FlatStyle = 'Flat'
Alternatively, you can store the button object directly in an aux. variable with a fixed name:
Set-Variable "Button$_" ($button = [System.Windows.Forms.Button]::new())
$button.FlatStyle = 'Flat'
Note how the variable assignment ($button = ...) is part of the constructor expression.
I would rather use a collection to store button attributes and loop through it to create one by one from that.
Then you have full control on the attributes of the button that you want to create.
$Buttons = #(
#{ Name='ABC'; Val=100; FlatStye='Flat'; Xpos=35; Ypos=30}
#{ Name='EFG'; Val=101; FlatStye='Flat'; Xpos=35; Ypos=60 }
#{ Name='XYZ'; Val=102; FlatStye='Popup'; Xpos=35; Ypos=90 }
#{ Name='MNL'; Val=102; FlatStye='Popup'; Xpos=35; Ypos=120 }
)
Function Generate-Form {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# prepare Form
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(400,400)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
# Add Buttons from collection
foreach ($btn in $Buttons) {
$Button = [System.Windows.Forms.Button] #{
FlatStyle = $btn.FlatStye
Name = $btn.Name
Text = $btn.Name
Location = New-Object System.Drawing.Size($btn.Xpos, $btn.Ypos )
Size = New-Object System.Drawing.Size(220,25)
}
#Add Button event
$Button.Add_Click({
[System.Windows.Forms.MessageBox]::Show($Button.Text , "My Dialog Box")
})
$Form.Controls.Add($Button)
}
#Show the Form
$form.ShowDialog()| Out-Null
} #End Function
Generate-Form;

Tray Icon Context Menu without hidden Form

I've been experimenting with tray icons & context menus in PowerShell for some time. However, i can only get the context menu to work correctly when a Form is called in the same script.
Here is a small example:
Add-Type -AssemblyName "System.Windows.Forms"
$objForm = New-Object System.Windows.Forms.Form
$objNotifyIcon = New-Object System.Windows.Forms.NotifyIcon
$objContextMenu = New-Object System.Windows.Forms.ContextMenu
$objExitMenuItem = New-Object System.Windows.Forms.MenuItem
$objExitMenuItem.Index = 1
$objExitMenuItem.Text = "Exit"
$objExitMenuItem.add_Click({
$objForm.Close()
$objNotifyIcon.visible = $false
})
$objContextMenu.MenuItems.Add($objExitMenuItem) | Out-Null
$objNotifyIcon.Icon = "$PSScriptRoot\win.ico"
$objNotifyIcon.Text = "Context Menu"
$objNotifyIcon.ContextMenu = $objContextMenu
$objForm.ContextMenu = $objContextMenu
#Enabling Icon in Taskbar
$objNotifyIcon.Visible = $true
#Hiding Form as best as possible
$objForm.Visible = $false
$objForm.WindowState = "minimized"
$objForm.ShowInTaskbar = $false
$objForm.add_Closing({ $objForm.ShowInTaskBar = $False })
$objForm.ShowDialog()
As soon as the Form componets are removed, the Context menu wont work correctly.
Does anyone know why you need this Form to be loaded and is there a way around it?
System.Windows.Forms.ApplicationContext is what you need to use to acheive that.