PowerShell Add_Click in foreach loop - powershell

What I am trying to accomplish is to create buttons that launch exe files in a certain directory when clicked, but when I try using a foreach loop to create a few buttons, all of the buttons just launch the file the last button is supposed to launch.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Main Window'
$form.Size = New-Object System.Drawing.Size(600,400)
$flp = New-Object System.Windows.Forms.FlowLayoutPanel
$flp.Location = New-Object System.Drawing.Point(0,0)
$flp.Height = $form.Height
$flp.Width = $form.Width
$form.Controls.Add($flp)
$files = Get-ChildItem "$home\Downloads" -Include *.exe -Name
foreach ($file in $files){
$button = New-Object System.Windows.Forms.Button
$flp.Controls.Add($button)
$button.Width = 100
$button.Height = 50
$button.Text = $file
$button.Add_Click{
Start-Process -FilePath "$home\Downloads\$file"
}
}
$form.Topmost = $true
$form.ShowDialog()
Whatever I'm doing is probably pretty stupid, so I was just looking for any alternatives or solutions to this other than to just hard code everything.

It is likely that you need to use .GetNewClosure() ScriptBlock method so that each script block (button click event) holds the current value of the $file variable at the moment of enumeration.
Example of what this means:
$blocks = foreach($i in 0..5) {
{ "hello $i" }
}
& $blocks[0] # => hello 5
& $blocks[1] # => hello 5
$blocks = foreach($i in 0..5) {
{ "hello $i" }.GetNewClosure()
}
& $blocks[0] # => hello 0
& $blocks[1] # => hello 1
In that sense, and assuming this is the issue, the following should work:
foreach ($file in $files) {
$button = New-Object System.Windows.Forms.Button
$button.Width = 100
$button.Height = 50
$button.Text = $file
$thisEvent = {
Start-Process -FilePath "$home\Downloads\$file"
}.GetNewClosure()
$button.Add_Click($thisEvent)
$flp.Controls.Add($button)
}
A nice alternative to having a need to use .GetNewClosure() can be seen on this answer. The .Tag property of the Button can be used to store the information of the file's path which then can be used on the button's .Click event:
foreach ($file in $files) {
$button = New-Object System.Windows.Forms.Button
$button.Width = 100
$button.Height = 50
$button.Text = $file
# Store the file's path in the Tag's property of this Button
$button.Tag = "$home\Downloads\$file"
$button.Add_Click({
Start-Process -FilePath $this.Tag
})
$flp.Controls.Add($button)
}

Related

Loading Data from .csv into DataGridView Form

I am trying to load the contents of a .csv table into a DataGridView. In the following code it is loaded at two point with the same method: after a button press and in the main routine.
The one in the main routine works, but the button doesn't work.
(The DataGrid just goes blank after pressing the button, so it apprears it's not a scope issue).
#############################################
#Functions
#############################################
function SelectCsvFile () {
Add-Type -AssemblyName System.Windows.Forms
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.Title = "Datei auswählen"
$OpenFileDialog.InitialDirectory = Get-Location
$OpenFileDialog.filter = 'Tabelle (*.csv)|*.csv'
$SelectedFile=$OpenFileDialog.ShowDialog()
If ($SelectedFile -eq "Cancel") {
$result = $null
}
else {
$result= new-object System.Collections.ArrayList
$data=#(import-csv $OpenFileDialog.FileName -Delimiter $ListImportDelimiter)
$result.AddRange($data)
}
$OpenFileDialog.Dispose()
return $result
}
#############################################
#Main Routine
#############################################
# Init PowerShell Gui
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
# Create a new form
$ManageTeamsForm = New-Object system.Windows.Forms.Form
# Define the size, title and background color
$ManageTeamsForm.ClientSize = New-Object System.Drawing.Point(1000,700)
#DataGrid
$DataGrid = new-object System.windows.forms.DataGridView
$DataGrid.width = 800
$DataGrid.height = 500
$DataGrid.location = New-Object System.Drawing.Point(37,80)
#loades testing csv on startup
$result= new-object System.Collections.ArrayList
$data=#(import-csv "./testfile.csv" -Delimiter $ListImportDelimiter)
$result.AddRange($data)
$DataGrid.DataSource = $result
#Select File Button
$SelectCsvFileButton = New-Object System.Windows.Forms.Button
$SelectCsvFileButton.Location = New-Object System.Drawing.Size(100,35)
$SelectCsvFileButton.Size = New-Object System.Drawing.Size(120,23)
$SelectCsvFileButton.Text = "Open CSV"
$SelectCsvFileButton.Add_Click({
$DataGrid.DataSource =SelectCsvFile;
ForEach ($row in $DataGrid.DataSource ){ #Testing if DataSource successfully changes
write-host $row
}
})
$ManageTeamsForm.controls.AddRange(#($SelectCsvFileButton,$DataGrid))
[void]$ManageTeamsForm.ShowDialog()
Your code is correct the only issue is that the output from your function SelectCsvFile is being enumerated, your function is no longer outputting an ArrayList but an object[] or a single object PSCumstomObject.
This is more or less explained in about Script Blocks:
A script block returns the output of all the commands in the script block, either as a single object or as an array.
You can overcome this by using the comma operator , which will wrap your ArrayList in a single element array to avoid losing the output type.
To put it into perspective with a simple example:
(& { [System.Collections.ArrayList]#(0..10) }).GetType() # Object[]
(& { , [System.Collections.ArrayList]#(0..10) }).GetType() # ArrayList
So, changing your function as follows should solve your problem:
function SelectCsvFile {
$fileDialog = [System.Windows.Forms.OpenFileDialog]#{
Title = "Datei auswählen"
InitialDirectory = Get-Location
Filter = 'Tabelle (*.csv)|*.csv'
}
if($fileDialog.ShowDialog() -eq 'OK') {
, [System.Collections.ArrayList]#(Import-Csv $fileDialog.FileName -Delimiter $ListImportDelimiter)
}
}

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
}

How to display picturebox with GUI in the windows form using Powershell?

I want to show the picturebox, but it does not show once I use form.show(). But if I change to Form.showdialog, the picturebox will show but, the process can not continue until I close the GUI. The picture box show but it does not moving, it stuck like picture.
Function Handling
{
$Form.Close()
$Form.Dispose()
$Form = New-Object system.Windows.Forms.Form
$Form.ControlBox = $true
$Form.BackColor = "#d0021b"
$Form.WindowState = "Maximized"
$Form.TopMost = $false
[void]$Form.Show()
# Message Box
[System.Windows.MessageBox]::Show("OK", "[Error]", "0", "Error")
$ExitCode = "1"
if($ExitCode -ne "107A")
{
$Form.Close()
$Form.Dispose()
Exit
}
else{
$Form.Close()
$Form.Dispose()
Exit
}
}
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$Form = New-Object system.Windows.Forms.Form
$form.BackgroundImageLayout = 'Center'
$Form.WindowState = 'Maximized'
$Form.BackColor = "#ffffff"
$file2 = Get-ChildItem -Path "D:\3.png"
$cover = [Drawing.Image]::FromFile($file2)
$form.BackgroundImage = $img2
[reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
$file = (get-item 'D:\6.gif')
$img = [System.Drawing.Image]::Fromfile($file)
$pictureBox = new-object Windows.Forms.PictureBox
$pictureBox.Image = $img
$pictureBox.SizeMode = "Autosize"
$pictureBox.Anchor = "Bottom, left"
$Form.controls.add($pictureBox)
[reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
$file3 = (get-item 'D:\6.gif')
$img3 = [System.Drawing.Image]::Fromfile($file3)
$pictureBox2 = new-object Windows.Forms.PictureBox
$pictureBox2.Image = $img3
$pictureBox2.SizeMode = "Autosize"
$pictureBox2.Anchor = "Bottom, right"
$Form.controls.add($pictureBox2)
$form.Show()
Write-Host "next process"
####
# some process
###
Start-Sleep -s 2
Handling
Anyone can give me idea please. Really appreciate for your help. Thank you.
Can you try like this? Put your background operation in place of Start-Sleep -s 2 and the gif file still show moving.
$Form = New-Object system.Windows.Forms.Form
$Form.Location= New-Object System.Drawing.Size(100,100)
$Form.Size= New-Object System.Drawing.Size(550,170)
$Form.StartPosition = "Manual"
$Form.Visible=$false
$Form.Enabled = $true
$Form.Add_Shown({$Form.Activate()})
[reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
$file = (get-item 'D:\6.gif')
$img = [System.Drawing.Image]::Fromfile($file);
[System.Windows.Forms.Application]::EnableVisualStyles();
$pictureBox = new-object Windows.Forms.PictureBox
$pictureBox.Location = New-Object System.Drawing.Size(0,1)
$pictureBox.Size = New-Object System.Drawing.Size($img.Width,$img.Height)
$pictureBox.Image = $img
$Form.controls.add($pictureBox)
$WaitForm.Topmost = $True
$rs = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs.Open()
$rs.SessionStateProxy.SetVariable("Form", $Form)
$data = [hashtable]::Synchronized(#{text=""})
$rs.SessionStateProxy.SetVariable("data", $data)
$p = $rs.CreatePipeline({ [void] $Form.ShowDialog()})
$p.Input.Close()
$p.InvokeAsync()
## Enter the rest of your script here while you want the form to display
Start-Sleep -s 2
$WaitForm.close()

How to show windows form while running the process using PowerShell?

I want to show the windows form while I do the process.
I try this, but It only show the form, but the process is not running until I close the form.
Anyone can give idea please.
Thank you
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object Windows.Forms.Form
$Form.WindowState = 'Maximized'
$Form.ControlBox = $false
$file = Get-ChildItem -Path "D:\picture.png"
$img = [Drawing.Image]::FromFile($file)
$form.BackgroundImage = $img
$form.BackgroundImageLayout = 'Center'
$Form.BackColor = "#ffffff"
[void]$Form.ShowDialog()
Function AAA
{
##process
}
Function BB
{
##process
}
$random = ([char[]]([char]'A'..[char]'Z') + 0..9 | sort {get-random})[0..7] -join ''
Write-Host "Random: $random"
Start-Sleep -s 2
$Exit = "0"

Don’t display Cancel in PowerShell script result

I have the following PowerShell script which displays file dialog to select a txt file. If user cancels dialog then provide a multiline text box
function GetDetails() {
Add-Type -AssemblyName System.Windows.Forms;
$browser = New-Object System.Windows.Forms.OpenFileDialog;
$browser.Filter = "txt (*.txt)|*.txt";
$browser.InitialDirectory = "E:\";
$browser.Title = "select txt file";
$browserResult = $browser.ShowDialog();
if($browserResult -eq [System.Windows.Forms.DialogResult]::OK) {
$nfoFile = $browser.FileName;
if([string]::IsNullOrWhiteSpace($txtFile)) {
return GetFromForm;
}
$txtFile = [System.IO.Path]::ChangeExtension($nfoFile, ".dac");
$txtFile = $temp + [System.IO.Path]::GetFileName($txtFile);
$exeArgs = "-f -S `"$txtFile`" -O `"$txtFile`"";
Start-Process $anExe -ArgumentList $exeArgs -Wait;
$result = Get-Content $txtFile | Out-String;
$browser.Dispose();
return $result;
} else {
return GetFromForm;
}
}
function GetFromForm(){
Add-Type -AssemblyName System.Windows.Forms;
$form = New-Object System.Windows.Forms.Form;
$form.Width = 800;
$form.Height = 600;
$txtBox = New-Object System.Windows.Forms.TextBox;
$txtBox.Multiline = $true;
$txtBox.AcceptsReturn = $true;
$txtBox.AcceptsTab = $true;
$txtBox.Visible = $true;
$txtBox.Name = "txtName";
$txtBox.Width = 760;
$txtBox.Height = 660;
$form.Controls.Add($txtBox);
$form.ShowDialog();
 
$form.Dispose();
return $txtBox.Text;
}
$desc = GetDetails;
cls;
Write-Host $desc;
Here I have two issues:
In Write-Host $desc, prints also Cancel hereiswhateverstrimg string if user chose to cancel dialog. How to avoid that?
If I run script in ISE, the generated form (in second function) will be always behind ISE even I call ShowDialog(), I expected to behave as modal dialog. It’s normal or there is a fix for this ?
Ok, there are a few changes that I made for efficiency and a few for functionality. Read the comments in the script for the explanations.
# Just add types once. There is no need to add the types in each function.
Add-Type -AssemblyName System.Windows.Forms;
Add-Type -AssemblyName System.Drawing
function GetDetails() {
$browser = New-Object System.Windows.Forms.OpenFileDialog;
$browser.Filter = "txt (*.txt)|*.txt";
$browser.InitialDirectory = "E:\";
$browser.Title = "select txt file";
$browserResult = $browser.ShowDialog();
# Combined the if statements
if($browserResult -eq [System.Windows.Forms.DialogResult]::OK -and [string]::IsNullOrWhiteSpace($txtFile) -ne $true) {
$nfoFile = $browser.FileName;
$txtFile = [System.IO.Path]::ChangeExtension($nfoFile, ".dac");
$txtFile = $temp + [System.IO.Path]::GetFileName($txtFile);
$exeArgs = "-f -S `"$txtFile`" -O `"$txtFile`"";
Start-Process $anExe -ArgumentList $exeArgs -Wait;
# The Raw flag should return a string
$result = Get-Content $txtFile -Raw;
$browser.Dispose();
return $result;
}
# No need for else since the if statement returns
return GetFromForm;
}
function GetFromForm(){
$form = New-Object System.Windows.Forms.Form;
$form.Text = 'Adding Arguments'
$form.Size = New-Object System.Drawing.Size(816,600)
$form.StartPosition = 'CenterScreen'
# Added a button
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(585,523)
$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)
$txtBox = New-Object System.Windows.Forms.TextBox;
$txtBox.Multiline = $true;
$txtBox.AcceptsReturn = $true;
$txtBox.AcceptsTab = $true;
$txtBox.Visible = $true;
$txtBox.Name = "txtName";
$txtBox.Size = New-Object System.Drawing.Size(660,500)
$form.Controls.Add($txtBox);
# Needed to force it to show on top
$form.TopMost = $true
# Select the textbox and activate the form to make it show with focus
$form.Add_Shown({$txtBox.Select(), $form.Activate()})
# Finally show the form and assign the ShowDialog method to a variable (this keeps it from printing out Cancel)
$result = $form.ShowDialog();
# If the user hit the OK button return the text in the textbox
if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
return $txtBox.Text
}
}
$desc = GetDetails;
cls;
Write-Host $desc;
You can see reference material here: https://learn.microsoft.com/en-us/powershell/scripting/samples/creating-a-custom-input-box?view=powershell-6
You need to suppress output of $form.ShowDialog() in GetFromForm:
$form.ShowDialog()|out-null
Powershell will add to a return value everything that was outputted to host within a function/commandlet.
Regarding your second issue - see this answer
And please do not use semi-colon at an end of line. This is not C# and will confuse you into thinking that the line is ended here but it's not quite true.