I have included a progress bar in a script. When I run the script the bar exists (since the related window is listed when I browse the opened windows with Alt+Tab) but I cannot select and see it.
Here there is my chunk of code...
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName PresentationFramework
[...]
# progress bar
$form_bar = New-Object System.Windows.Forms.Form
$form_bar.Text = "TRANSFER RATE"
$form_bar.Size = New-Object System.Drawing.Size(600,200)
$form_bar.StartPosition = "manual"
$form_bar.Location = '1320,840'
$font = New-Object System.Drawing.Font("Arial", 12)
$form_bar.Font = $font
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(20,20)
$label.Size = New-Object System.Drawing.Size(550,30)
$form_bar.Controls.Add($label)
$bar = New-Object System.Windows.Forms.ProgressBar
$bar.Style="Continuous"
$bar.Location = New-Object System.Drawing.Point(20,70)
$bar.Maximum = 101
$bar.Size = New-Object System.Drawing.Size(550,30)
$form_bar.Controls.Add($bar)
$form_bar.Topmost = $true
$form_bar.Show() | out-null
$form_bar.Focus() | out-null
[...]
$percent = ($trasferred_bytes / $total_bytes)*100
$formatted = '{0:0.0}' -f $percent
[int32]$progress = $percent
$CurrentTime = $Time.Elapsed
$estimated = [int]((($CurrentTime.TotalSeconds/$percent) * (100 - $percent)) / 60)
$label.Text = "Progress: $formatted% - $estimated mins to end"
if ($progress -ge 100) {
$bar.Value = 100
} else {
$bar.Value = $progress
}
$form_bar.Refresh()
There are two basic approaches to showing a WinForms form from PowerShell:
Event-based: Show the form modally, using its .ShowDialog()
method:
This blocks execution of your PowerShell script until the form is closed.
Therefore, any operations you want to perform must be performed in event handlers attached to the form or its controls, including, potentially, a timer control whose events run periodically.
However, you cannot run lengthy operations in event handlers without blocking the form's event processing, so the best solution is to use a PowerShell background job (see below).
Loop-based: Show the form non-modally, using its .Show()
method:
This continues execution of your PowerShell script.
Since PowerShell remains in control of the foreground thread, the form is unresponsive by default (which is what you're experiencing).
Therefore, you must enter a loop while the form is being displayed, in which you periodically call [System.Windows.Forms.Application]::DoEvents()
in order to keep the form responsive, typically complemented with Start-Sleep
to avoid a tight loop.
[System.Windows.Forms.Application]::DoEvents()
calls; for long-running blocking operations, you'll have to use a background job too, whose progress you can monitor in the loop (see below).Sample code:
The following simplified, self-contained, PSv5+ samples illustrate the two approaches.
A caveat re event-handler script blocks is that they run in a child scope of the caller; while you can get the caller's variables directly, setting them requires use of the $script:
scope specifier, in the simplest case - see this answer.
Start-Job
is used for the simulated long-running background operation; however, in PowerShell (Core) 7+, it is preferable to use the faster and more efficient Start-ThreadJob
cmdlet instead; you can also use it in Windows PowerShell, if you install it on demand; see this answer.
Note that in the loop-based solution you may not always need to use a background [thread] job; if the code between progress-bar updates of the progress bar runs fairly quickly, you can run it directly in the loop.
As the samples show, the loop-based solution is simpler and conceptually more straightforward.
Event-based sample:
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
$maxProgressSteps = 10
# Create the form.
$form = [Form] @{
Text = "TRANSFER RATE"; Size = [Size]::new(600, 200); StartPosition = 'CenterScreen'; TopMost = $true; MinimizeBox = $false; MaximizeBox = $false; FormBorderStyle = 'FixedSingle'
}
# Add controls.
$form.Controls.AddRange(@(
($label = [Label] @{ Location = [Point]::new(20, 20); Size = [Size]::new(550, 30) })
($bar = [ProgressBar] @{ Location = [Point]::new(20, 70); Size = [Size]::new(550, 30); Style = 'Continuous'; Maximum = $maxProgressSteps })
))
# Create a timer and register an event-handler script block
# that periodically checks the background job for new output
# and updates the progress bar accordingly.
($timer = [Timer] @{ Interval = 200 }).add_Tick({
# Note: This code runs in a *child scope* of the script.
if ($output = Receive-Job $job) {
$step = $output[-1] # Use the last object output.
# Update the progress bar.
$label.Text = '{0} / {1}' -f $step, $maxProgressSteps
$bar.Value = $step
}
if ($job.State -in 'Completed', 'Failed') { $form.Close() }
})
# Enable the timer when the form loads.
$form.add_Load({
$timer.Enabled = $true
})
# Start the long-running background job that
# emits objects as they become available.
$job = Start-Job {
foreach ($i in 1..$using:maxProgressSteps) {
$i
Start-Sleep -Milliseconds 500
}
}
# Show the form *modally*, i.e. as a blocking dialog.
$null = $form.ShowDialog()
# Getting here means that the form was closed.
# Clean up.
$timer.Dispose(); $form.Dispose()
Remove-Job $job -Force
Loop-based sample:
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
$maxProgressSteps = 10
# Create the form.
$form = [Form] @{
Text = "TRANSFER RATE"; Size = [Size]::new(600, 200); StartPosition = 'CenterScreen'; TopMost = $true; MinimizeBox = $false; MaximizeBox = $false; FormBorderStyle = 'FixedSingle'
}
# Add controls.
$form.Controls.AddRange(@(
($label = [Label] @{ Location = [Point]::new(20, 20); Size = [Size]::new(550, 30) })
($bar = [ProgressBar] @{ Location = [Point]::new(20, 70); Size = [Size]::new(550, 30); Style = 'Continuous'; Maximum = $maxProgressSteps })
))
# Start the long-running background job that
# emits objects as they become available.
$job = Start-Job {
foreach ($i in 1..$using:maxProgressSteps) {
$i
Start-Sleep -Milliseconds 500
}
}
# Show the form *non-modally*, i.e. execution
# of the script continues, and the form is only
# responsive if [System.Windows.Forms.Application]::DoEvents() is called periodically.
$null = $form.Show()
while ($job.State -notin 'Completed', 'Failed') {
# Check for new output objects from the background job.
if ($output = Receive-Job $job) {
$step = $output[-1] # Use the last object output.
# Update the progress bar.
$label.Text = '{0} / {1}' -f $step, $maxProgressSteps
$bar.Value = $step
}
# Allow the form to process events.
[System.Windows.Forms.Application]::DoEvents()
# Sleep a little, to avoid a near-tight loop.
# IMPORTANT: Do NOT use Start-Sleep, as certain events -
# notably reactivating a minimized window from the taskbar - then do not work.
[Threading.Thread]::Sleep(100)
}
# Clean up.
$form.Dispose()
Remove-Job $job -Force