1

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()
  • 1
    That is probably because it runs in a single process with the rest of your script. Meaning that you have to make your script event driven or even multithreading see: [How to handle progress bar using PowerShell?](https://stackoverflow.com/a/58147372/1701026) – iRon Jun 24 '21 at 11:16
  • 2
    `$CurrentTime = $Time.Elapsed`: wrong Timer. Use a System.Windows.Forms.Timer. Or set the System.Timers.Timer's [SynchronizingObject](https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer.synchronizingobject) property to the Form instance. -- No need to `Refresh()` the Form, the ProgressBar already does that on its own. – Jimi Jun 24 '21 at 12:47

1 Answers1

1

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.

      • Note: The code in the loop must not itself be long-running blocking operations, as that would preclude regular [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
mklement0
  • 382,024
  • 64
  • 607
  • 775