Created
June 5, 2025 06:57
-
-
Save gravejester/9aa5f7bbeca88cb370dee919a65209b9 to your computer and use it in GitHub Desktop.
WPF Progress Bar in PowerShell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function New-wpfProgressBar { | |
param( | |
[string] $WindowTitle = 'Progress', | |
[int] $WindowWidth = 450, | |
[int] $WindowHeight = 140, | |
[string] $Text = "Processing...", | |
[switch] $IsIndeterminate, | |
[switch] $ShowPercentage, | |
[switch] $Topmost | |
) | |
try { | |
# load .NET classes needed for wpf | |
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase | |
$ErrorActionPreference = 'Stop' | |
# hastable to handle communication between functions | |
# as well as between calling thread and the UI thread | |
$GLOBAL:pbSharedData = [hashtable]::Synchronized(@{ }) | |
$pbSharedData.WindowReady = $false | |
$pbSharedData.ShouldClose = $false | |
$pbSharedData.IsIndeterminate = $IsIndeterminate | |
# pack input parameters into a single object | |
$params = @{ | |
WindowTitle = $WindowTitle | |
WindowWidth = $WindowWidth | |
WindowHeight = $WindowHeight | |
IsIndeterminate = $IsIndeterminate | |
ShowPercentage = $ShowPercentage | |
Text = $Text | |
Topmost = $Topmost | |
} | |
# lets create a new runspace for our UI thread to live in | |
$runspace = [runspacefactory]::CreateRunspace() | |
$runspace.ApartmentState = 'STA' | |
$runspace.ThreadOptions = 'ReuseThread' | |
$runspace.Open() | |
# the UI thread need access to pbSharedData and the parameters | |
$runspace.SessionStateProxy.SetVariable('pbSharedData', $GLOBAL:pbSharedData) | |
$runspace.SessionStateProxy.SetVariable('params', $params) | |
# add a reference to the runspace itself in pbSharedData (just in case) | |
$pbSharedData.Runspace = $runspace | |
# create a new powershell process for the ui thread | |
$uiThread = [powershell]::Create() | |
# and inject the code that we want to run | |
[void]$uiThread.AddScript({ | |
# this is our UI window | |
$window = New-Object -TypeName System.Windows.Window | |
$window.Title = $params.WindowTitle | |
$window.Width = $params.WindowWidth | |
$window.Height = $params.WindowHeight | |
$window.WindowStartupLocation = 'CenterScreen' | |
$window.ResizeMode = 'NoResize' | |
$window.WindowStyle = 'SingleBorderWindow' | |
$window.Topmost = $params.Topmost | |
$pbSharedData.Window = $window | |
# grids for layout | |
$grid = New-Object -TypeName System.Windows.Controls.Grid | |
$grid.Margin = '20' | |
$r1 = New-Object -TypeName System.Windows.Controls.RowDefinition | |
$r1.Height = 'Auto' | |
$r2 = New-Object -TypeName System.Windows.Controls.RowDefinition | |
$r2.Height = '*' | |
[void]$grid.RowDefinitions.Add($r1) | |
[void]$grid.RowDefinitions.Add($r2) | |
$pbGrid = New-Object -TypeName System.Windows.Controls.Grid | |
$pbGrid.Height = 25 | |
$pbGrid.Margin = '0,0,0,10' | |
[System.Windows.Controls.Grid]::SetRow($pbGrid, 0) | |
# the progress bar | |
$progressBar = New-Object -TypeName System.Windows.Controls.ProgressBar | |
$progressBar.Name = 'ProgressBar' | |
$progressBar.Minimum = 0 | |
$progressBar.Maximum = 100 | |
$progressBar.Value = 0 | |
$progressBar.IsIndeterminate = $params.IsIndeterminate | |
[void]$pbGrid.Children.Add($progressBar) | |
# show percent complete text on the bar (if applicable) | |
if ($ShowPercentage) { | |
$percentageText = New-Object -TypeName System.Windows.Controls.TextBlock | |
$percentageText.Name = 'PercentageText' | |
$percentageText.Text = '0%' | |
$percentageText.HorizontalAlignment = 'Center' | |
$percentageText.VerticalAlignment = 'Center' | |
$percentageText.FontWeight = 'Bold' | |
$percentageText.Foreground = 'Black' | |
[void]$pbGrid.Children.Add($percentageText) | |
} | |
[void]$grid.Children.Add($pbGrid) | |
# the progress text | |
$progressText = New-Object -TypeName System.Windows.Controls.TextBlock | |
$progressText.Name = 'ProgressText' | |
$progressText.Text = $params.Text | |
$progressText.HorizontalAlignment = 'Left' | |
$progressText.Margin = '2,0,10,10' | |
$progressText.VerticalAlignment = 'Top' | |
$progressText.TextWrapping = 'Wrap' | |
$progressText.FontSize = 12 | |
[System.Windows.Controls.Grid]::SetRow($progressText, 1) | |
[void]$grid.Children.Add($progressText) | |
# add the main grid to the window | |
$pbSharedData.Window.Content = $grid | |
# show the wpf window | |
$pbSharedData.WindowReady = $true | |
$pbSharedData.Window.Show() | |
# lets add any error messages to pbSharedData | |
$pbSharedData.Error = $Error | |
# ui thread message loop | |
while (-not $pbSharedData.ShouldClose -and $pbSharedData.Window.IsVisible) { | |
# check for updates to the progress text | |
if ($pbSharedData.ProgressText) { | |
$progressText.Text = $pbSharedData.ProgressText | |
} | |
# check for updates to the progress bar value | |
if ($pbSharedData.ProgressBarValue) { | |
$progressBar.Value = $pbSharedData.ProgressBarValue | |
} | |
# check for updates to the percentage complete text | |
if ($pbSharedData.PercentageText) { | |
$percentageText.Text = $pbSharedData.PercentageText | |
} | |
# update ui thread | |
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke( | |
[System.Windows.Threading.DispatcherPriority]::Background, | |
[System.Action] {}) | |
# for the IsIndeterminate animation to play smoothly we need a quicker loop | |
if ($params.IsIndeterminate) { | |
Start-Sleep -Milliseconds 10 | |
} else { | |
Start-Sleep -Milliseconds 500 | |
} | |
} | |
# if we get this far, we are out of the loop above | |
# and if we are - it means we want to close the UI window | |
if ($pbSharedData.Window.IsVisible) { | |
$pbSharedData.Window.Close() | |
} | |
}) | |
# tell the ui thread to use the new runspace we created | |
$uiThread.Runspace = $runspace | |
# invoke the code in the ui thread | |
$invokeHandle = $uiThread.BeginInvoke() | |
# save a reference in pbSharedData | |
$pbSharedData.invokeHandle = $invokeHandle | |
} catch { Write-Warning "Error creating progress window: $($_.Exception.Message)" } | |
} | |
function Close-wpfProgressBar { | |
if ($pbSharedData) { | |
try { | |
$pbSharedData.ShouldClose = $true | |
Start-Sleep -Milliseconds 300 | |
try { | |
# some clean-up of the runspace (if it exists) | |
$pbSharedData.Runspace.Close() | |
$pbSharedData.Runspace.Dispose() | |
} catch {} | |
# delete the shared hashtable variable | |
Remove-Variable -Name 'pbSharedData' -Scope Global -Force | |
} catch { Write-Warning "Error closing progress window: $($_.Exception.Message)" } | |
} | |
} | |
function Write-wpfProgressBar { | |
param( | |
[ValidateRange(0, 100)] | |
[int]$PercentComplete = 0, | |
[string]$Text | |
) | |
try { | |
$ErrorActionPreference = 'Stop' | |
if (-not $pbSharedData) { Write-Warning "pbSharedData not found - please use New-wpfProgressBar!"; return } | |
# update progress text | |
$pbSharedData.ProgressText = $Text | |
# update progress bar vale | |
$pbSharedData.ProgressBarValue = $PercentComplete | |
# update percentage complete text | |
$pbSharedData.PercentageText = "$PercentComplete%" | |
} catch { Write-Warning "Failed to update progress window: $($_.Exception.Message)" } | |
} | |
<# | |
# Example usage: | |
# Create a standard progress window with percentage display | |
New-wpfProgressBar -WindowTitle "File Processing" -Text "Starting file processing..." -ShowPercentage | |
# Simulate some work with progress updates | |
for ($i = 1; $i -le 10; $i++) { | |
Start-Sleep -Milliseconds 500 | |
$percent = ($i / 10) * 100 | |
Write-wpfProgressBar -PercentComplete $percent -Text "Processing file $i of 10..." | |
} | |
Close-wpfProgressBar | |
# Example with indeterminate progress bar | |
New-wpfProgressBar -WindowTitle "Loading Data" -IsIndeterminate -Text "Loading, please wait..." | |
Start-Sleep -Seconds 5 | |
Close-wpfProgressBar | |
#> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment