Get the lines of the TextBlock according to the TextWrapping property?
Asked Answered
R

3

8

I have a TextBlock in WPF application.

The (Text, Width, Height, TextWrapping, FontSize, FontWeight, FontFamily) properties of this TextBlock is dynamic (entered by the user at the runtime).

Every time the user changes one of the previous properties, the Content property of the TextBlock is changed at the runtime. (everything is ok until here)

Now, I need to get the lines of that TextBlock according to the previously specified properties.
That means I need the lines that TextWrapping algorithms will result.

In other words, I need each line in a separated string or I need one string with Scape Sequence \n.

Any Idea to do that?

Rattletrap answered 21/5, 2015 at 15:42 Comment(5)
Can you show your code too?Senatorial
@Hakam, I am right that you want to count the number of lines your code is spread on, right ?Orsino
@EmmanuelDURIN No I do not want to know the count of the lines, I want to know the content of each line. For each line I want to know the char that the line start in, and the char that the line end in. In other words I want to get the result of the text after applying the TextWrapping algorithm on itClamorous
@Hakam,I ve spent some time watching the code of TextBlock, thinking there could be an easy solution. Having the count of lines is pretty easy if you can read a private property. But for having the content of the TextBlock, you 'd need to access several (10 ?) private/protected/internal members and some few internal/private classes. So may be it'd be easier writing your own component, drawing your text that'd you formatted yourself - controlling the content of each line. It is possible. There is such a class in .Net.Tell me if you re interestedOrsino
@EmmanuelDURIN what is this class offer me exactly? can you explain what the services that this class provide?Clamorous
S
11

I would have been surprised if there is no public way of doing that (although one never knows, especially with WPF).
And indeed looks like TextPointer class is our friend, so here is a solution based on the TextBlock.ContentStart, TextPointer.GetLineStartPosition and TextPointer.GetOffsetToPosition:

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);
    }
}

There is not much to explain here
Get the start position of the line, subtract the start position of the previous line to get the length of the line text and here we are.
The only tricky (or non obvious) part is the need to offset the ContentStart by one since by design The TextPointer returned by this property always has its LogicalDirection set to Backward., so we need to get the pointer for the same(!?) position, but with LogicalDirection set to Forward, whatever all that means.

Spencer answered 16/10, 2015 at 14:56 Comment(4)
first : thanks for your answer. Second : I briefly test your solution (not so deeply and it worked with me. Third : The solution by @II Vic does NOT crash (at least it does not crashed with my tests)Clamorous
I really prefer a solution without diving inside the private property, and I am very glad that there is solution like this. I had to say that the solution of II Vic is good, but if there is solution without using the private members of the class, this would be better.Clamorous
@HakamFostok Sorry, I didn't want to be offensive to him, it really was crashing in my tests (trying to compare if they provide one and the same results). However when I run only his code, it doesn't crash! Thanks for pointing that (although I should have known that). I researched it, and here is the case - just add the following line at the beginning of his OnCalculateClick var contentStart = txt.ContentStart;. Pretty innocent, right? Now his code crashes! Anyway, I'm going to remove that part from my answer.Spencer
I agree with @HakamFostok: this is for sure the best solution and the most elegant one! It doesn't use reflection so it is preferable to mine!Variform
O
4

With FormattedText class, the formatted text can be first created and its size evaluated, so you know the space it takes in a first step, If it's too long, it's up to you to split in separate lines.

Then in a second step, it could be drawn.

Everything could happen on the DrawingContext object in the following method :

protected override void OnRender(System.Windows.Media.DrawingContext dc)

Here is the CustomControl solution :

[ContentProperty("Text")]
public class TextBlockLineSplitter : FrameworkElement
{
    public FontWeight FontWeight
    {
        get { return (FontWeight)GetValue(FontWeightProperty); }
        set { SetValue(FontWeightProperty, value); }
    }

    public static readonly DependencyProperty FontWeightProperty =
        DependencyProperty.Register("FontWeight", typeof(FontWeight), typeof(TextBlockLineSplitter), new PropertyMetadata(FontWeight.FromOpenTypeWeight(400)));

    public double FontSize
    {
        get { return (double)GetValue(FontSizeProperty); }
        set { SetValue(FontSizeProperty, value); }
    }

    public static readonly DependencyProperty FontSizeProperty =
        DependencyProperty.Register("FontSize", typeof(double), typeof(TextBlockLineSplitter), new PropertyMetadata(10.0));

    public String FontFamily
    {
        get { return (String)GetValue(FontFamilyProperty); }
        set { SetValue(FontFamilyProperty, value); }
    }

    public static readonly DependencyProperty FontFamilyProperty =
        DependencyProperty.Register("FontFamily", typeof(String), typeof(TextBlockLineSplitter), new PropertyMetadata("Arial"));

    public String Text
    {
        get { return (String)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(String), typeof(TextBlockLineSplitter), new PropertyMetadata(null));

    public double Interline
    {
        get { return (double)GetValue(InterlineProperty); }
        set { SetValue(InterlineProperty, value); }
    }

    public static readonly DependencyProperty InterlineProperty =
        DependencyProperty.Register("Interline", typeof(double), typeof(TextBlockLineSplitter), new PropertyMetadata(3.0));

    public List<String> Lines
    {
        get { return (List<String>)GetValue(LinesProperty); }
        set { SetValue(LinesProperty, value); }
    }

    public static readonly DependencyProperty LinesProperty =
        DependencyProperty.Register("Lines", typeof(List<String>), typeof(TextBlockLineSplitter), new PropertyMetadata(new List<String>()));

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        Lines.Clear();
        if (!String.IsNullOrWhiteSpace(Text))
        {
            string remainingText = Text;
            string textToDisplay = Text;
            double availableWidth = ActualWidth;
            Point drawingPoint = new Point();

            // put clip for preventing writing out the textblock
            drawingContext.PushClip(new RectangleGeometry(new Rect(new Point(0, 0), new Point(ActualWidth, ActualHeight))));
            FormattedText formattedText = null;

            // have an initial guess :
            formattedText = new FormattedText(textToDisplay,
                Thread.CurrentThread.CurrentUICulture,
                FlowDirection.LeftToRight,
                new Typeface(FontFamily),
                FontSize,
                Brushes.Black);
            double estimatedNumberOfCharInLines = textToDisplay.Length * availableWidth / formattedText.Width;

            while (!String.IsNullOrEmpty(remainingText))
            {
                // Add 15%
                double currentEstimatedNumberOfCharInLines = Math.Min(remainingText.Length, estimatedNumberOfCharInLines * 1.15);
                do
                {
                    textToDisplay = remainingText.Substring(0, (int)(currentEstimatedNumberOfCharInLines));

                    formattedText = new FormattedText(textToDisplay,
                        Thread.CurrentThread.CurrentUICulture,
                        FlowDirection.LeftToRight,
                        new Typeface(FontFamily),
                        FontSize,
                        Brushes.Black);
                    currentEstimatedNumberOfCharInLines -= 1;
                } while (formattedText.Width > availableWidth);

                Lines.Add(textToDisplay);
                System.Diagnostics.Debug.WriteLine(textToDisplay);
                System.Diagnostics.Debug.WriteLine(remainingText.Length);
                drawingContext.DrawText(formattedText, drawingPoint);
                if (remainingText.Length > textToDisplay.Length)
                    remainingText = remainingText.Substring(textToDisplay.Length);
                else
                    remainingText = String.Empty;
                drawingPoint.Y += formattedText.Height + Interline;
            }
            foreach (var line in Lines)
            {
                System.Diagnostics.Debug.WriteLine(line);
            }
        }
    }
}

Usage of that control (border is here to show effective clipping) :

<Border BorderThickness="1" BorderBrush="Red" Height="200" VerticalAlignment="Top">
    <local:TextBlockLineSplitter>Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do. Once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, &quot;and what is the use of a book,&quot; thought Alice, ...</local:TextBlockLineSplitter>
</Border>
Orsino answered 12/10, 2015 at 9:43 Comment(2)
So I have to write the algorithm of the splitting the text according to the properties (Like Width, Height, TextWrapping ... ) by myself, Right?Clamorous
You re right. Not very fun to write a class close to TextBlock, but compared to digging into calling multiple (may be 10 or 20 ) private members/classes of TextBlock, I think it's cleaner. TextBlock is pretty powerful for managing Flow , so it is complicated. Note tht FormattedText makes the job of computing size of textOrsino
V
2

If it is not a problem you can use reflection on the TextBlock control (it of course knows how the string is wrapped). If you are not using MVVM, I guess it is suitable for you.

First of all I created a minimal window for testing my solution:

<Window x:Class="WpfApplication1.MainWindow" Name="win"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="600" Width="600">

    <StackPanel>
        <TextBlock Name="txt"  Text="Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua." Margin="20" 
                   TextWrapping="Wrap" />
        <Button Click="OnCalculateClick" Content="Calculate ROWS" Margin="5" />

        <TextBox Name="Result" Height="100" />
    </StackPanel>

</Window>

Now let's see the most important part of the code-behind:

private void OnCalculateClick(object sender, EventArgs args)
{
    int start = 0;
    int length = 0;

    List<string> tokens = new List<string>();

    foreach (object lineMetrics in GetLineMetrics(txt))
    {
        length = GetLength(lineMetrics);
        tokens.Add(txt.Text.Substring(start, length));

        start += length;
    }

    Result.Text = String.Join(Environment.NewLine, tokens);
}

private int GetLength(object lineMetrics)
{
    PropertyInfo propertyInfo = lineMetrics.GetType().GetProperty("Length", BindingFlags.Instance
        | BindingFlags.NonPublic);

    return (int)propertyInfo.GetValue(lineMetrics, null);
}

private IEnumerable GetLineMetrics(TextBlock textBlock)
{
    ArrayList metrics = new ArrayList();
    FieldInfo fieldInfo = typeof(TextBlock).GetField("_firstLine", BindingFlags.Instance
        | BindingFlags.NonPublic);
    metrics.Add(fieldInfo.GetValue(textBlock));

    fieldInfo = typeof(TextBlock).GetField("_subsequentLines", BindingFlags.Instance
        | BindingFlags.NonPublic);

    object nextLines = fieldInfo.GetValue(textBlock);
    if (nextLines != null)
    {
        metrics.AddRange((ICollection)nextLines);
    }

    return metrics;
}

The GetLineMetrics method retrieves a collection of LineMetrics (an internal object, so I cannot use it directly). This object has a property called "Length" which has the information that you need. So the GetLength method just read this property's value.

Lines are stored in the list named tokens and showed by using the TextBox control (just to have an immediate feedback).

I hope my sample can help you in your task.

Variform answered 12/10, 2015 at 13:48 Comment(3)
Thanks a lot, I tested this, and it really worked as charm. Thank you again. I just need to do some more testing before accept the answer and give you the bounty.Clamorous
@HakamFostok, even if it works perfectly for you please don't hurry with bounty awarding. It attracts people. People upvote and/or provide even more help. You can get better answers (or just more useful information), while answer writers can get more upvotes plus your bounty at the end.Loader
@Loader thanks for advise, and yes I will do that, I will not hurry about giving the award because I am really want more information, although this is the best solution for this time and It really solve my problem, but I will delay the awarding of the bounty until the period finish.Clamorous

© 2022 - 2024 — McMap. All rights reserved.