PowerShell and WPF: TextBlock

Working again on my series on WPF and PowerShell, we are now going to take a look at TextBlocks, which is a lightweight control for displaying small amounts of flow content,  and how they can be used in your GUI to display text. You might be thinking to yourself, “why in the world would we need to look at yet another way to display text?”. Whereas a label and textbox and in fact display text to the user, it can only display it in one font, one color, etc… With a TextBlock, you can actually change all of these styles at any given point in time which can prove useful if your are parsing a log and want to highlight different types of data you find, which makes it like a RichTextbox (that I used in my project, PoshChat) with a few differences that will be shown later on in the article.

The following example shows some examples of using the TextBlock to display various styles for text.

#Build the GUI
[xml]$xaml = @"
<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen" ResizeMode="NoResize"
    Width = "313" Height = "425" ShowInTaskbar = "True" Background = "lightgray"> 
    <StackPanel >
        <TextBlock x:Name = 'textblock' TextWrapping = "Wrap">
            <TextBlock.Inlines>
                <Run FontWeight="Bold" FontSize="14" Text="This is WPF TextBlock Example in Bold. " />
                <LineBreak />
                <Run FontStyle="Italic" Foreground="Green" Text="This is green text with italics. " />
                <LineBreak />
                <Run FontStyle="Italic" FontSize="18" Text="Here is some linear gradient text. ">
                    <Run.Foreground>
                        <LinearGradientBrush>
                            <GradientStop Color="Red" Offset="0.0" />
                            <GradientStop Color="Orange" Offset="0.25" />
                            <GradientStop Color="Yellow" Offset="0.5" />
                            <GradientStop Color="Blue" Offset="0.75" />
                            <GradientStop Color="Green" Offset="1" />
                        </LinearGradientBrush>
                    </Run.Foreground>
                </Run>
                <Run FontStyle="Italic" Foreground="Blue" Text="How about adding some Blue to the mix? " />
            </TextBlock.Inlines>
        </TextBlock>
        <Label Background="Black"/>
        <TextBlock TextWrapping = "Wrap">
            Some <Italic> italic words </Italic> along with some <Bold> bold words </Bold> to show
            another way of using TextBlocks.
        </TextBlock>
    </StackPanel>
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

#Connect to Controls
$textblock = $Window.FindName('textblock')


$Window.ShowDialog() | Out-Null

image

The first TextBlock shows how you can use the Inlines property that you can then add text styles to using the Run class or inserting a line break using the LineBreak class. The second TextBlock is a little less involved, but you can still notice how I was able to add different font styles in with the text to create the italic words and the bold words.

Below is an example that shows a “logging window” that displays errors, warning, etc… with a specified color for the output.

$uiHash = [hashtable]::Synchronized(@{})
$runspaceHash = [hashtable]::Synchronized(@{})
$jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.Arraylist))
$uiHash.jobFlag = $True
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$uiHash)          
$newRunspace.SessionStateProxy.SetVariable("runspaceHash",$runspaceHash)     
$newRunspace.SessionStateProxy.SetVariable("jobs",$jobs)     
$psCmd = [PowerShell]::Create().AddScript({  
    Add-Type –assemblyName PresentationFramework
    Add-Type –assemblyName PresentationCore
    Add-Type –assemblyName WindowsBase  
    #Build the GUI
    [xml]$xaml = @"
    <Window 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen" ResizeMode="NoResize"
        Width = "313" Height = "425" ShowInTaskbar = "True" Background = "lightgray"> 
        <StackPanel >    
            <ScrollViewer x:Name = "scrollviewer" VerticalScrollBarVisibility="Visible"  Height="365">    
                <TextBlock x:Name = 'textblock' TextWrapping = "Wrap" />
            </ScrollViewer >            
            <Label Background="Black" />
            <Button x:Name="button" Content="Start Demo" Background="White" />
        </StackPanel>
    </Window>
"@
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $uiHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #Connect to Controls
    $uiHash.textblock = $uiHash.Window.FindName('textblock')
    $uiHash.button = $uiHash.Window.FindName('button')
    $uiHash.scrollviewer = $uiHash.Window.FindName('scrollviewer')

    #Jobs runspace
    $runspace = [runspacefactory]::CreateRunspace()
    $runspace.Open()
    $runspace.SessionStateProxy.SetVariable("uihash",$uihash)
    $runspace.SessionStateProxy.SetVariable("jobs",$jobs)
    $runspaceHash.PowerShell = [powershell]::Create().AddScript({
        While ($uihash.jobFlag) {
            If ($jobs.Handle.IsCompleted) {
                $jobs.PowerShell.EndInvoke($jobs.handle)
                $jobs.PowerShell.Dispose()
                $jobs.clear()
            }
        }
    })
    $runspaceHash.PowerShell.Runspace = $runspace
    $runspaceHash.Handle = $runspaceHash.PowerShell.BeginInvoke()

    $running = $False

    #Events
    $uiHash.Window.Add_Closed({
        $uiHash.jobFlag = $False
        sleep -Milliseconds 500
        $runspaceHash.PowerShell.EndInvoke($runspaceHash.Handle)
        $runspaceHash.PowerShell.Dispose()
        $runspaceHash.Clear()
    })

    $uiHash.button.Add_Click({
        Write-Verbose ("Running: {0}" -f $running) -Verbose
        Switch ($running) {
            $True {
                #Stop demo
                $uiHash.Flag = $False
                $uiHash.button.Content = 'Start Demo'
                $Script:running = $False
            }
            $False {
                $uiHash.Flag = $True
                $scriptBlock = {
                    While ($uiHash.Flag){                    
                        Start-Sleep -Milliseconds 500
                        $uiHash.textblock.Dispatcher.Invoke("Normal",[action]{
                            $messages = "ERROR: Something bad has just happened!",
                                        "WARNING: A potential issue could occurr!",
                                        "VERBOSE: Making a change from this to that.",
                                        "INFO: Everything is going according to plan."                        
                            $Run = New-Object System.Windows.Documents.Run
                            $message = Get-Random $messages
                            Write-Verbose ("Type: {0}" -f $message) -Verbose
                            Switch -regex ($message) {
                                "^Verbose" {
                                    $Run.Foreground = "Yellow"
                                }
                                "^Warning" {
                                    $Run.Foreground = "Blue"
                                }
                                "^Info" {
                                    $Run.Foreground = "Black"
                                }
                                "^Error" {
                                    $Run.Foreground = "Red"
                                }
                            }
                            $Run.Text = ("{0}" -f $message)
                            Write-Verbose ("Adding a new line") -Verbose
                            $uiHash.TextBlock.Inlines.Add($Run)
                            Write-Verbose ("Adding a new linebreak") -Verbose
                            $uiHash.TextBlock.Inlines.Add((New-Object System.Windows.Documents.LineBreak))                                                  
                        })
                        $uiHash.scrollviewer.Dispatcher.Invoke("Normal",[action]{
                            $uiHash.scrollviewer.ScrollToEnd()
                        })
                    }
                }
                $runspace = [runspacefactory]::CreateRunspace()
                $runspace.Open()
                $runspace.SessionStateProxy.SetVariable("uiHash",$uiHash)
                $temp = "" | Select PowerShell,Handle
                $temp.PowerShell = [powershell]::Create().AddScript($scriptBlock)
                $temp.PowerShell.Runspace = $runspace
                $temp.Handle = $temp.PowerShell.BeginInvoke()
                $jobs.Add($temp)
                #Start demo
                $uiHash.button.Content = 'Stop Demo'
                $Script:running = $True
            }
        }
    })
    $uiHash.Window.ShowDialog() | Out-Null  
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

image

There are a couple of important things here to look at. The first thing is the use of a vertical scrollbar to handle the data as it goe s beyond the height of the window. A TextBlock does not have the capability to use a scrollbar unlike a TextBox or RichTextBox. To get around this issue, I have to wrap the TextBlock in a ScrollViewer control which then allows me to have a scrollbar on the TextBlock. Just as important is that I also set the size of the TextBlock through the ScrollViewer, otherwise the scrolling will not work like you think it should.

The next item to look at is how the focus is always on the last line being displayed. Regardless of how much data is being processed, the focus will always be at the bottom. This is done using the ScrollToEnd() method from the ScrollViewer. As long as the method is called after writing the line of text, it will always ensure that the data is shown and saves you from scrolling to the bottom to find out what is going on.

On a side note, you will notice that I am using background runspaces to handle all of my data and displaying of information. This is a great idea if you have GUIs that have long running operations. More on the use of that can be found here.

About Boe Prox

Microsoft Cloud and Datacenter MVP working as a SQL DBA.
This entry was posted in powershell, WPF and tagged , , . Bookmark the permalink.

4 Responses to PowerShell and WPF: TextBlock

  1. Jonathan says:

    Hi,
    First thanks for your amazing works on multithread WPF interfaces for powershell, it really helps me a lot…
    I Just have a small issue in my project and hope perhaps you could suggest me a hint to circumvent my issue.
    We are working on a project to create a tool for user with some reporting and links to some documentation
    I use a powershell script to create a wpf GUI and I want to put some text in textblock but Icannot find a way to insert hyperlink into. The hyperlink is here, clickable, but I cannot manage to find a way to do something when the link is clicked.
    I check lots of tutorial and how to on the web but it seems that we need to use VB code to make an action on hyperlink event. could we register event handler in powershell to effectively open a browser when this hyperlink is clicked ?

    I could post my code if it could helps

    Best regards,
    Jonathan

  2. Damir says:

    Thank You so much for all your posts. I almost gave up on trying to use WPF, as didn’t find good enough, by now, tutorial. This is great!

    I have a problem with execution of your last script in this article from a command line. If I do it in powershell ise or powergui, no problems but from command line, basically nothing happens. Window doesn’t open, no error messages, … I just get command line back.

    I call your script, as I always do: powershell -sta full path to the file name
    It worked on all other scripts you have provided, if I include
    Add-Type –assemblyName PresentationFramework
    at the beginning, so my guess is that I am missing type?

    • Boe Prox says:

      Because I am using background runspaces to handle the GUI thread and another runspace to handle the background commands, when you run it using powershell.exe -file that is thinks that nothing is actually running and closes out. You should add the -NoExit switch to it and that should let it work for you.

      • Damir says:

        Thank you. It worked.
        Looking forward to your next posts, covering more controls. This is the best place I have found for WPF in powershell. Thank You for great posts again.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s