Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell Progress Bar in Windows Forms

Tags:

powershell

I'm trying to add a progress bar to a form in powershell. I do not want to use PowerShell's Write-Progress cmdlet (because when I run the script from command line, it shows a text-based progress bar and I always want a form/graphic based bar).

I've tried this and it seems to work(found online):

[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null

$form_main = New-Object System.Windows.Forms.Form
$progressBar1 = New-Object System.Windows.Forms.ProgressBar
$timer1 = New-Object System.Windows.Forms.Timer

$timer1_OnTick = {
  $progressBar1.PerformStep()
}

$form_main.Text = 'ProgressBar demo'

$progressBar1.DataBindings.DefaultDataSourceUpdateMode = 0
$progressBar1.Step = 1
$progressBar1.Name = 'progressBar1'

$form_main.Controls.Add($progressBar1)

$timer1.Interval = 100
$timer1.add_tick($timer1_OnTick)
$timer1.Start()

$form_main.ShowDialog()| Out-Null

However, I do not want an event to update the progress bar (as does $timer1_OnTic in the example above) I want to update it myself by making calls throughout my script such as:

$progressBar1.PerformStep()

Or

$progressBar1.Value = 10

So it seems I need some sort of background worker that updates the progress bar whenever I make calls to PerformStep() or change the value of the progressBar

Calling ShowDialog stops all processing inside the script until the form is closed.

like image 904
user577240 Avatar asked Apr 13 '11 01:04

user577240


2 Answers

If I understand correctly, you should be able to change ShowDialog() to Show(), which will display the Dialog without blocking your script. You can then continue execution and update the progress bar.

You may be disappointed in the lack of interactivity of the form though.

like image 111
beefarino Avatar answered Sep 29 '22 15:09

beefarino


A method I have had some success with is to use a child runspace for the GUI (in this case WPF) so it doesn't lock the script. Data can be accessed in both the parent and sub runspaces via the session state proxy.

e.g.

# define the shared variable
$sharedData = [HashTable]::Synchronized(@{});
$sharedData.Progress = 0;
$sharedData.state = 0;
$sharedData.EnableTimer = $true;

# Set up the runspace (STA is required for WPF)
$rs = [RunSpaceFactory]::CreateRunSpace();
$rs.ApartmentState = "STA";
$rs.ThreadOptions = "ReuseThread";
$rs.Open();

# configure the shared variable as accessible from both sides (parent and child runspace)
$rs.SessionStateProxy.setVariable("sharedData", $sharedData);

# define the code to run in the child runspace
$script = {
    add-Type -assembly PresentationFramework;
add-Type -assembly PresentationCore;
add-Type -assembly WindowsBase;

[xml]$xaml = @"
<Window
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  MaxHeight="100" MinHeight="100" Height="100" 
  MaxWidth="320" MinWidth="320" Width="320" 
  WindowStyle="ToolWindow">

<Canvas Grid.Row="1">
  <TextBlock Name="ProgressText" Canvas.Top="10" Canvas.Left="20">Hello world</TextBlock>
  <ProgressBar Name="ProgressComplete" Canvas.Top="30" Canvas.Left="20" Width="260" Height="20" HorizontalAlignment="Center" Value="20" />
</Canvas>

</Window>
"@

    # process the xaml above
    $reader = New-Object System.Xml.XmlNodeReader $xaml;
    $dialog = [Windows.Markup.XamlReader]::Load($reader);

    # get an handle for the progress bar
    $progBar = $dialog.FindName("ProgressComplete");
    $progBar.Value = 0;

    # define the code to run at each interval (update the bar)
    # DON'T forget to include a way to stop the script
    $scriptBlock = {
        if ($sharedData.EnableTimer = $false) {
            $timer.IsEnabled = $false;
            $dialog.Close();
        }

        $progBar.value = $sharedData.Progress;
    }

    # at the timer to run the script on each 'tick'
    $dialog.Add_SourceInitialized( {
        $timer = new-Object System.Windows.Threading.DispatherTimer;
        $timer.Interface = [TimeSpan]"0:0:0.50";
        $timer.Add_Tick($scriptBlock);
        $timer.Start();
        if (!$timer.IsEnabled) {
            $dialog.Close();
        }
    });

    # Start the timer and show the dialog
    &$scriptBlock;
    $dialog.ShowDialog() | out-null;
}

$ps = [PowerShell]::Create();
$ps.Runspace = $rs;
$ps.AddScript($script).BeginInvoke();

# if you want data from your GUI, you can access it through the $sharedData variable
Write-Output $sharedData;

If you try this code, once the dialog is displayed you can change the progress bar by setting the value of $sharedData.Progress

This has allowed me to write plenty of dialogs for tools, I'm constrained by our infrastructure to use powershell from within a runspace and WPF seems to work much better than forms.

like image 20
Djarid Avatar answered Sep 29 '22 14:09

Djarid