Get TextBlock strings according to TextWrapping property?

I have a TextBlock in a WPF application.

The < Text , Width , Height , TextWrapping , FontSize , FontWeight , FontFamily ) FontWeight this TextBlock are dynamic (entered by the user in Runtime).

Each time the user changes one of the previous properties, the Content TextBlock property changes at run time. (everything is fine while here)

Now I need to get the rows of this TextBlock according to the previously specified properties.
This means that I need strings that will lead to TextWrapping algorithms.

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

Any idea to do this?

+5
source share
3 answers

I would be surprised if there was no public way to do this (although no one knows, especially with WPF).
And it really looks like the TextPointer class is our friend, so here is a solution based on 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); } } 

Nothing to explain here
Get the starting position of the line, subtract the starting 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 ContentStart by one, because the The TextPointer returned by this property always has its LogicalDirection set to Backward. design The TextPointer returned by this property always has its LogicalDirection set to Backward. , so we need to get a pointer to the same (!?) position, but with a LogicalDirection set to Forward , whatever that means.

+5
source

With the FormattedText class, you can first create formatted text and estimate its size so that you know how much space it takes in the first step. If it is too long, you need to separate separate lines.

Then in the second stage it could be drawn.

Everything can happen on the DrawingContext following way:

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

Using this control (border shown here for 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> 
+4
source

If this is not a problem, you can use reflection in the TextBlock control (he, of course, knows how the string is wrapped). If you are not using MVVM, I think it suits you.

First of all, I created a minimal window to test 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 look at the most important part of the code:

 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 the LineMetrics collection (an internal object, so I cannot use it directly). This object has the property "Length", which has the information you need. So the GetLength method just read this property value.

Lines are stored in a list called tokens and displayed using the TextBox control (for immediate feedback only).

I hope my sample will help you in your task.

+2
source

All Articles