Search code examples
powershellwinformsprogress-bar

Progress bar exists but it is not visible


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()

Solution

  • 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