Search code examples
wpfvb.netxamltextblockword-wrap

Preserving indenting when wrapping in a wpf textblock control


I have a WPF textblock set up with the property TextWrapping="Wrap".

When I pass in a long string with a tab character (vbTab in my case) at the start, I would like the wrapping to honour this and keep the wrapped parts of the string indented. For example, instead of:

[vbTab]thisisreallylong

andwrapped

I want

[vbTab]thisisreallylong

[vbTab]andwrapped

and ideally for multiple tabs, etc. too.

[edit - additional details]

Because the textblock will be of variable size and contain multiple lines of text with various amounts of indenting, I can't just have a margin or manually split the strings and add tabs.

Essentially what I want is for it to treat lines of text like paragraphs, that keep their indenting when they wrap.


Solution

  • Based on your idea, I am able to come up with this solution

    I'll convert all the tabs in the beginning of every line to .5 inch margin each and will add the same text in a paragraph and apply the calculated margin to the same

    A TextBlock was not feasible for the same as it is useful for basic text inlines like run bold, inline ui container etc. adding paragraph was more complicated in a TextBlock so I made the solution based on FlowDocument.

    Result

    result

    below example demonstrate the same using FlowDocumentScrollViewer or RichTextBox or FlowDocumentReader or plain FlowDocument

    I have created the solution using attached properties, so you can attach the same to any of the mentioned or even add your own host for the document. you simply have to set IndentationProvider.Text to the desired host.

    XAML

    <Window x:Class="MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:sys="clr-namespace:System;assembly=mscorlib"
            xmlns:l="clr-namespace:PreservingIndentationDemo"
            Title="MainWindow"
            Height="350"
            Width="525">
        <Window.Resources>
            <sys:String x:Key="longString"
                        xml:space="preserve">&#x09;this is really long and wrapped
            
    &#x09;&#x09;another line this is also really long and wrapped
            
    &#x09;one more line this is also really long and wrapped
            
    another line this is also really long and wrapped
            
    &#x09;&#x09;another line this is also really long and wrapped
            </sys:String>
        </Window.Resources>
        <Grid>
            <FlowDocumentScrollViewer l:IndentationProvider.Text="{StaticResource longString}" />
            <!--<RichTextBox l:TextToParaHelper.Text="{StaticResource longString}" IsReadOnly="True"/>-->
            <!--<FlowDocumentReader l:TextToParaHelper.Text="{StaticResource longString}" />-->
            <!--<FlowDocument l:TextToParaHelper.Text="{StaticResource longString}" />-->
        </Grid>
    </Window>
    

    &#x09; refers to tab char

    IndentationProvider

    Class IndentationProvider
    
        Public Shared Function GetText(obj As DependencyObject) As String
            Return DirectCast(obj.GetValue(TextProperty), String)
        End Function
    
        Public Shared Sub SetText(obj As DependencyObject, value As String)
            obj.SetValue(TextProperty, value)
        End Sub
    
        ' Using a DependencyProperty as the backing store for Text.  This enables animation, styling, binding, etc...
        Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.RegisterAttached("Text", GetType(String), GetType(IndentationProvider), New PropertyMetadata(Nothing, AddressOf OnTextChanged))
    
        Private Shared Sub OnTextChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
            Dim blocks As BlockCollection = Nothing
    
            Dim rtb As RichTextBox = TryCast(d, RichTextBox)
            If rtb IsNot Nothing Then
                rtb.Document.Blocks.Clear()
                blocks = rtb.Document.Blocks
            End If
    
            If blocks Is Nothing Then
                Dim fd As FlowDocument = TryCast(d, FlowDocument)
                If fd IsNot Nothing Then
                    fd.Blocks.Clear()
                    blocks = fd.Blocks
                End If
            End If
    
            If blocks Is Nothing Then
                Dim fdr As FlowDocumentReader = TryCast(d, FlowDocumentReader)
                If fdr IsNot Nothing Then
                    fdr.Document = New FlowDocument()
                    blocks = fdr.Document.Blocks
                End If
            End If
    
            If blocks Is Nothing Then
                Dim fdr As FlowDocumentScrollViewer = TryCast(d, FlowDocumentScrollViewer)
                If fdr IsNot Nothing Then
                    fdr.Document = New FlowDocument()
                    blocks = fdr.Document.Blocks
                End If
            End If
    
            Dim newValue As String = TryCast(e.NewValue, String)
            If Not String.IsNullOrWhiteSpace(newValue) Then
                For Each line As String In newValue.Split(ControlChars.Lf)
                    Dim leftMargin As Double = 0
                    Dim newLine As String = line
                    While newLine.Length > 0 AndAlso newLine(0) = ControlChars.Tab
                        leftMargin += 0.5
                        newLine = newLine.Remove(0, 1)
                    End While
                    Dim marginInch As String = leftMargin & "in"
                    Dim marginDip As Double = CDbl(New LengthConverter().ConvertFromString(marginInch))
    
                    Dim para As New Paragraph(New Run(newLine)) With {.Margin = New Thickness(marginDip, 0, 0, 0)}
                    blocks.Add(para)
                Next
            End If
        End Sub
    End Class
    

    Demo

    try demo project