Mohamed Ashraf
Mohamed Ashraf

Reputation: 301

How to get TextBlock lines with Inline element support after applying TextWrapping?

I'm trying to split a very long string (document) on several pages containing a TextBlock, however, I need to make each page of specific number of lines which means that I need to split the TextBlock into lines.

I tried to create several logics but no luck of getting an accurate thing, but found a solution here (Get the lines of the TextBlock according to the TextWrapping property?) which worked for me on my prototype project then stopped working and gets the whole text in one line.

Here is the code from the above topic:

public static class TextUtils
    {
        public static IEnumerable<string> GetLines(this TextBlock source)
        {
            var text = source.Text;
            int offset = 0;
            TextPointer lineStart = source.ContentStart.GetPositionAtOffset(1, LogicalDirection.Forward);
            do
            {
                TextPointer lineEnd = lineStart != null ? lineStart.GetLineStartPosition(1) : null;
                int length = lineEnd != null ? lineStart.GetOffsetToPosition(lineEnd) : text.Length - offset;
                yield return text.Substring(offset, length);
                offset += length;
                lineStart = lineEnd;
            }
            while (lineStart != null);
        }
    }

And this is my code:

<TextBlock x:Name="testTB" TextAlignment="Justify" FontFamily="Arial" FontSize="12" TextWrapping="Wrap" Width="100"/>
testTB.Text = Functions.GenString(200);

foreach (string xc in testTB.GetLines())
{
    MessageBox.Show(xc);
}

Where I guess that the issue is that lineStart.GetLineStartPosition(1) is returning null.

Any help is appreciated, thanks in advance.

Upvotes: 2

Views: 1163

Answers (1)

BionicCode
BionicCode

Reputation: 28968

To me the code you have posted looks error prone. It will work only if the TextBlock contains plain text. But when you are using Inline elements like Run, Bold or Underline, you no more have plain text as content, but also context markers like tags for the inline elements. I guess this is where your offset based string.Substring fails.

The solution is to create a TextRange from the retrieved TextPointer results and extract the plain text via the TextRange.Text property.

The following implementation supports both: plain text set via the TextBlock.Text property and text set using Inline elements:

public static IEnumerable<string> GetLines(this TextBlock source)
{
  TextPointer lineStart = source.ContentStart.GetPositionAtOffset(1, LogicalDirection.Forward);
  do
  {
    TextPointer lineEnd = lineStart.GetLineStartPosition(1) ?? source.ContentEnd; 
    var textRange = new TextRange(lineStart, lineEnd);
    lineStart = lineEnd;
    yield return textRange.Text;
  }
  while (lineStart.IsAtLineStartPosition);
}

Remarks

It is important to wait until the TextBlock.Loaded event was raised. This is because the TextBlock splits the single text string into lines during the UIElement.Measure process, as this is the moment where the control knows its desired size and therefore the max available width of a line. UIElement.Measure is invoked by the rendering engine, when the layout loading has started.

Example

MainWindow.xaml

<Window>
  <TextBlock x:Name="TextBlock" 
             TextWrapping="Wrap"
             Width="100">
    <TextBlock.Inlines>
      <Run
        Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat," />
      <Bold>
        <Bold.Inlines>
          <Run Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, conse" />
        </Bold.Inlines>
      </Bold>
      <LineBreak />
      <LineBreak />
      <LineBreak />
      <Underline>
        <Run Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut " />
        <Run Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut " />
      </Underline>
      <Run Text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut " />
    </TextBlock.Inlines>
  </TextBlock>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow()
  {
    this.Loaded += OnLoaded;
  }

  private void OnLoaded(object sender, EventArgs e)
  {
    var lines = this.TextBlock.GetLines().ToList(); // Returns 54 lines
  }
}

Upvotes: 2

Related Questions