OK so I am newer to using windows forms inside Powershell, and I am having a bit of trouble with the form being all laggy since there is a continuous ping running. Fundamentally all I need to do is display the IP address each time it runs a ping. I tried to search around and it seems like the proper way to do this is with a running job in the background?
I think the way this is currently written that the ping happens in the background but I'm not sure how to update the form (or if its even possible to make it visible to it?)
Here is a sample, any guidance with this would be greatly appreciated.
function global:ContinuousPing{
$global:job = start-job {
while($true){
$pingStatus = Test-Connection google.com -Count 1
$label.text = $pingStatus.IPV4Address.IPAddressToString
#[System.Windows.Forms.Application]::DoEvents()
start-sleep 1
}
}
}
Add-Type -AssemblyName System.Windows.Forms
$pingForm = New-Object System.Windows.Forms.Form
$label = New-Object System.Windows.Forms.Label
$button1 = New-Object System.Windows.Forms.Button
$ping = {
ContinuousPing
while($true){
Receive-Job $job
}
}
$label.Text = "Ping Status"
$button1.Location = New-Object System.Drawing.Point(100,10)
$button1.add_Click($ping)
$pingForm.Controls.Add($label)
$pingForm.Controls.Add($button1)
$pingForm.ShowDialog() | Out-Null
remove-job $job
You need to separate the GUI updating and the ping task. This is easy to in languages that are designed to have GUIs like C# winforms, If you tried it you would be surprised how much easier it is than trying to bend a GUI over PowerShell.
You can try the link Mathias posted, It will do what you require.
Related
I start building up a small winform with Copy button and a label under this button.
When I click on Copy button it starts to copy files from source to destination.
I would like to run this asynchroniously so I don't want form to be freezed while copy operation runs. That's why I use Job. After a successful copy I need feedback of copy and show an "OK" text with green color but it is not working.
Here is my code:
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.Application]::EnableVisualStyles()
Function Copy-Action{
$Computername = "testclient"
$Source_Path = "C:\temp\"
$Destination_Path = "\\$Computername\c$\temp"
$job = Start-Job -Name "Copy" -ArgumentList $Source_Path,$Destination_Path –ScriptBlock {
param($Source_Path,$Destination_Path)
Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force
}
Register-ObjectEvent $job StateChanged -MessageData $Status_Label -Action {
[Console]::Beep(1000,500)
$Status_Label.Text = "OK"
$Status_Label.ForeColor = "#009900"
$eventSubscriber | Unregister-Event
$eventSubscriber.Action | Remove-Job
} | Out-Null
}
# DRAW FORM
$form_MainForm = New-Object System.Windows.Forms.Form
$form_MainForm.Text = "Test Copy"
$form_MainForm.Size = New-Object System.Drawing.Size(200,200)
$form_MainForm.FormBorderStyle = "FixedDialog"
$form_MainForm.StartPosition = "CenterScreen"
$form_MainForm.MaximizeBox = $false
$form_MainForm.MinimizeBox = $true
$form_MainForm.ControlBox = $true
# Copy Button
$Copy_Button = New-Object System.Windows.Forms.Button
$Copy_Button.Location = "50,50"
$Copy_Button.Size = "75,30"
$Copy_Button.Text = "Copy"
$Copy_Button.Add_Click({Copy-Action})
$form_MainForm.Controls.Add($Copy_Button)
# Status Label
$Status_Label = New-Object System.Windows.Forms.Label
$Status_Label.Text = ""
$Status_Label.AutoSize = $true
$Status_Label.Location = "75,110"
$Status_Label.ForeColor = "black"
$form_MainForm.Controls.Add($Status_Label)
#show form
$form_MainForm.Add_Shown({$form_MainForm.Activate()})
[void] $form_MainForm.ShowDialog()
Copy is successful but showing an "OK" label won't. I have placed a Beep but it doesn't work too.
What am I doing wrong ? Any solution to this?
Thank you.
Let me offer alternatives to balrundel's helpful solution - which is effective, but complex.
The core problem is that while a form is being shown modally, with .ShowDialog(), WinForms is in control of the foreground thread, not PowerShell.
That is, PowerShell code - in the form's event handlers - only executes in response to user actions, which is why your job-state-change event handler passed to Register-ObjectEvent's -Action parameter does not fire (it would eventually fire, after closing the form).
There are two fundamental solutions:
Stick with .ShowDialog() and perform operations in parallel, in a different PowerShell runspace (thread).
balrundel's solution uses the PowerShell SDK to achieve this, whose use is far from trivial, unfortunately.
See below for a simpler alternative based on Start-ThreadJob
Show the form non-modally, via the .Show() method, and enter a loop in which you can perform other operations while periodically calling [System.Windows.Forms.Application]::DoEvents() in order to keep the form responsive.
See this answer for an example of this technique.
A hybrid approach is to stick with .ShowDialog() and enter a [System.Windows.Forms.Application]::DoEvents() loop inside the form event handler.
This is best limited to a single event handler applying this technique, as using additional simultaneous [System.Windows.Forms.Application]::DoEvents() loops invites trouble.
See this answer for an example of this technique.
Simpler, Start-ThreadJob-based solution:
Start-ThreadJob is part of the the ThreadJob module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.
It comes with PowerShell (Core) 7+ and can be installed on demand in Windows PowerShell with, e.g., Install-Module ThreadJob -Scope CurrentUser.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
In addition to syntactic convenience, Start-ThreadJob, due to being thread-based (rather than using a child process, which is what Start-Job does), allows manipulating the calling thread's live objects.
Note that the sample code below, in the interest of brevity, performs no explicit thread synchronization, which may situationally be required.
The following simplified, self-contained sample code demonstrates the technique:
The sample shows a simple form with a button that starts a thread job, and updates the form from inside that thread job after the operation (simulated by a 3-second sleep) completes, as shown in the following screen shots:
Initial state:
After pressing Start Job (the form remains responsive):
After the job has ended:
The .add_Click() event handler contains the meat of the solution; the source-code comments hopefully provide enough documentation.
# PSv5+
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
# Create a sample form.
$form = [Form] #{
Text = 'Form with Thread Job'
ClientSize = [Point]::new(200, 80)
FormBorderStyle = 'FixedToolWindow'
}
# Create the controls and add them to the form.
$form.Controls.AddRange(#(
($btnStartJob = [Button] #{
Text = "Start Job"
Location = [Point]::new(10, 10)
})
[Label] #{
Text = "Status:"
AutoSize = $true
Location = [Point]::new(10, 40)
Font = [Font]::new('Microsoft Sans Serif', 10)
}
($lblStatus = [Label] #{
Text = "(Not started)"
AutoSize = $true
Location = [Point]::new(80, 40)
Font = [Font]::new('Microsoft Sans Serif', 10)
})
))
# The script-level helper variable that maintains a collection of
# thread-job objects created in event-handler script blocks,
# which must be cleaned up after the form closes.
$script:jobs = #()
# Add an event handler to the button that starts
# the background job.
$btnStartJob.add_Click( {
$this.Enabled = $false # To prevent re-entry while the job is still running.
# Signal the status.
$lblStatus.Text = 'Running...'
$form.Refresh() # Update the UI.
# Start the thread job, and add the job-info object to
# the *script-level* $jobs collection.
# The sample job simply sleeps for 3 seconds to simulate a long-running operation.
# Note:
# * The $using: scope is required to access objects in the caller's thread.
# * In this simple case you don't need to maintain a *collection* of jobs -
# you could simply discard the previous job, if any, and start a new one,
# so that only one job object is ever maintained.
$script:jobs += Start-ThreadJob {
# Perform the long-running operation.
Start-Sleep -Seconds 3
# Update the status label and re-enable the button.
($using:lblStatus).Text = 'Done'
($using:btnStartJob).Enabled = $true
}
})
$form.ShowDialog()
# Clean up the collection of jobs.
$script:jobs | Remove-Job -Force
Start-Job creates a separate process, and when your form is ready to receive events, it can't listen to job events. You need to create a new runspace, which is able to synchronize thread and form control.
I adapted code from this answer. You can read much better explanation there.
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.Application]::EnableVisualStyles()
Function Copy-Action{
$SyncHash = [hashtable]::Synchronized(#{TextBox = $Status_Label})
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ThreadOptions = "UseNewThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)
$Worker = [PowerShell]::Create().AddScript({
$SyncHash.TextBox.Text = "Copying..."
# Copy-Item
$Computername = "testclient"
$Source_Path = "C:\temp\"
$Destination_Path = "\\$Computername\c$\temp"
Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force
$SyncHash.TextBox.ForeColor = "#009900"
$SyncHash.TextBox.Text = "OK"
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()
}
# DRAW FORM
$form_MainForm = New-Object System.Windows.Forms.Form
$form_MainForm.Text = "Test Copy"
$form_MainForm.Size = New-Object System.Drawing.Size(200,200)
$form_MainForm.FormBorderStyle = "FixedDialog"
$form_MainForm.StartPosition = "CenterScreen"
$form_MainForm.MaximizeBox = $false
$form_MainForm.MinimizeBox = $true
$form_MainForm.ControlBox = $true
# Copy Button
$Copy_Button = New-Object System.Windows.Forms.Button
$Copy_Button.Location = "50,50"
$Copy_Button.Size = "75,30"
$Copy_Button.Text = "Copy"
$Copy_Button.Add_Click({Copy-Action})
$form_MainForm.Controls.Add($Copy_Button)
# Status Label
$Status_Label = New-Object System.Windows.Forms.Label
$Status_Label.Text = ""
$Status_Label.AutoSize = $true
$Status_Label.Location = "75,110"
$Status_Label.ForeColor = "black"
$form_MainForm.Controls.Add($Status_Label)
#show form
$form_MainForm.Add_Shown({$form_MainForm.Activate()})
[void] $form_MainForm.ShowDialog()
EDIT : I was able to get it working, see below for my solution. The commenters below are correct that Powershell isn't really ideal for GUI's and threading, but it can be done.
I've got a form in Powershell that uses Start-Job to run functions in the background without freezing the GUI. My goal is to continuously check the status of those jobs for their output. I managed to use the Windows Forms Timer to check the results of the job and update the GUI accordingly.
It's all working fine, but it seems sloppy. Is this the best way to accomplish a GUI refresh? I'm relatively new to Powershell and I want to improve my coding.
Example of what I'm doing:
$jobScript = {
Start-Sleep 5
Write-Output "The job has finished running"
}
$timerScript = {
$timer.Stop()
$jobResult = Get-Job | Receive-Job -Keep
if ($jobResult) {
$btn.text = $jobResult
} else {
$timer.Start()
}
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "The job is still running"
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 100
$timer.add_Tick($timerScript)
$timer.Start()
Start-Job -ScriptBlock $jobScript
$form.ShowDialog()
Update: My solution
Using Register-ObjectEvent did not work, it seemed like it was fighting with the GUI for the thread. Instead I was able to use [System.Windows.Forms.Application]::DoEvents(). This allows the GUI to be moved around, and once it's done being moved, the thread will resume. The big caveat here is that execution is paused as long as the GUI is being moved, so if your code needs to react to the background job on a time limit, this could cause errors.
Example code block:
$jobScript =
{
Start-Sleep 5
Write-Output "The job is completed"
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "..."
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$btn.add_click({
$btn.Text = "Starting job"
$jobby = Start-Job -ScriptBlock $jobScript
Do {[System.Windows.Forms.Application]::DoEvents()} Until ($jobby.State -eq "Completed")
$btn.Text = Get-Job | Receive-Job
})
$form.ShowDialog()
You can use events:
$job = Start-Job {sleep 3; Write-Output "Dummy job completed"}
$callback = {
Write-Host "Event Fired"
Unregister-Event -SourceIdentifier "DummyJob"
Write-Host ($job | Receive-Job)
}
$evt = Register-ObjectEvent -InputObject $job -EventName StateChanged -SourceIdentifier "DummyJob" -Action $callback
# to remove all
Get-Job | Remove-Job -Force # jobs and events
Get-EventSubscriber | Unregister-Event # events
You might want to post this on CodeReview.StackExchange.com.
I sort of hate it when people build UI's in Powershell. If you want a proper Windows forms app, just write it in C#. So I disagree with the design at its premise.
I like your impulse to move away from the polling design; you initiate the job then poll to see if it's completed. I think an event handler might be a better choice. Check out the section "Monitor a Background Job" in the article PowerShell and Events: Object Events. It's an oldie but a goodie.
I would like to create a GUI that shows a loading bar while a job is running in the background. For simplicity, I've made the job an infinite loop so it should always be running. I've only included necessary parts of the code:
$Label = new-object system.windows.forms.Label
$Label.Font = 'Ariel,12pt'
$Label.Text = ""
$Label.AutoSize = $True
$Label.Location = new-object system.drawing.size(50,10)
$Form.Controls.Add($Label)
$LoadingAnimation = #(".....","0....",".0...","..0..","...0.","....0",".....")
$AnimationCount = 0
$test = start-job -Name Job -ScriptBlock { for($t=1;$t -gt 0; $t++){} }
while ($test.JobStateInfo.State -eq "Running")
{
$Label.Text = $LoadingAnimation[($AnimationCount)]
$AnimationCount++
if ($AnimationCount -eq $LoadingAnimation.Count){$AnimationCount = 0}
Start-Sleep -Milliseconds 200
}
Upon testing this code in the console, just using Write-Host instead of $Label.Text, it works just fine. What needs to be done differently to get this to work in a windows form created by PowerShell?
In PowerShell, you can create all sorts of status, including multi-level, using Write-Progress. Don't forget to call with -Completed when done (a common mistake I see).
Get-Help Write-Progress
After going through the little details of the script, I found the problem. This is how I activated my form:
$Form.Add_Shown({$Form.Activate()})
[void] $Form.ShowDialog()
This caused the script to stop when the form was launched, ShowDialog stops the script to allow interaction. The fix was:
$Form.Add_Shown({$Form.Activate()})
[void] $Form.Show()
Using Form.Show lets the script to continue to run because it doesn't require interaction.
My first question here, sorry if something is wrong in the way I posted.
Well, I'm new in developing graphic things in Powershell, and I needed to make a simple script to configure hostname, IP, etc and make it simple to end users (ugh).
I'm developing a simple graphic interface in PowerGUI, I'm on it for a couple of days, testing it exclusively inside PowerGUI, and it was not looking bad, but when I finally ran the script outside PowerGUI, just running it in Powershell, the graphics looks quite different than in PowerGUI, like positioning, borders, themes, etc. I was disappointing. I'll post screenshots and a bit of the code.
Inside PowerGUI
Outside
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0
ipconfig -all | Out-File $env:SYSTEMDRIVE"\old_network.txt"
#INICIO Fontes da aplicação
$font = New-Object System.Drawing.Font("Segoe UI",8.5,[System.Drawing.FontStyle]::Regular)
$font_btn_concluir = New-Object System.Drawing.Font("Segoe UI",15,[System.Drawing.FontStyle]::Regular)
$font_l_instru = New-Object System.Drawing.Font("Segoe UI",12,[System.Drawing.FontStyle]::Regular)
$font_i_n_sala = New-Object System.Drawing.Font("Segoe UI",20,[System.Drawing.FontStyle]::Regular)
#FIM Fontes da aplicação
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size #(800,600)
$form.StartPosition = "CenterScreen"
$form.Font = $font
#$form.ControlBox = $false
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.Text = "Configuração Telesalas - UNIASSELVI"
$form.Icon = $icon
$form.FormBorderStyle = "FixedDialog
So, anyone have any clue why this happens?
Its Because of the version of your forms. powergui automatically call v4.0** form my system but powershell consol calls v2. you can test it by yourself after compiling check the consols.
My script stops when I´m calling the message box or the voice output.
The script is waiting to complete the task what is the normal behavior in PowerShell.
But how it is possible to execute this script without a break at the voice output?
I want to execute this code without a break:
[System.Windows.Forms.MessageBox]::Show("stop")
$voice = new-object -com SAPI.SpVoice
$voice.Speak("Hello Stackoverflow!")
[System.Windows.Forms.MessageBox]::Show("done")
Like (Not working):
[System.Windows.Forms.MessageBox]::Show("stop")
$job = start-job {
$voice = new-object -com SAPI.SpVoice
$voice.Speak($text)
}
[System.Windows.Forms.MessageBox]::Show("done")
or like (Also not working):
$test = "Hello"
[System.Windows.Forms.MessageBox]::Show("stop")
$backPS = [powershell]::create()
[void] $backPS.AddScript("$voice = new-object -com SAPI.SpVoice
$voice.Speak($test)")
[System.Windows.Forms.MessageBox]::Show("done")
The background job approach works for me:
$job = start-job { (new-object -com SAPI.spVoice).Speak("hi") }
$text needs to either be passed to the background job or defined in the background job script block.
$job = start-job { (new-object -com SAPI.spVoice).Speak($args[0]) } -arg "hi"
The other blocking code is [System.Windows.Forms.MessageBox]::Show. Do you want this to be synchronous or asynchronous?