根据 TextWrapping 属性 获取 TextBlock 的行数?

Get the lines of the TextBlock according to the TextWrapping property?

我在 WPF 应用程序中有一个 TextBlock

(Text,Width,Height,TextWrapping,FontSize,FontWeight,FontFamily)属性其中 TextBlock 是动态的(由用户在运行时输入)。

每次用户更改先前的属性之一时,TextBlockContent 属性 都会在运行时更改。 (到这里为止一切正常)

现在,我需要根据先前指定的属性获取 TextBlock 的行。
这意味着我需要 TextWrapping 算法将产生的行。

换句话说,我需要在一个单独的字符串中的每一行,或者我需要一个带有 Scape Sequence \n.

的字符串

有什么想法吗?

使用 FormattedText class,可以首先创建格式化文本并评估其大小,因此您知道 space 它在第一步中需要的, 如果太长,请自行分行。

然后第二步就可以画出来了

以下方法中的 DrawingContext 对象可能发生任何事情:

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

这是 CustomControl 解决方案:

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

该控件的用法(边框在这里显示有效裁剪):

<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>

如果这不是问题,您可以在 TextBlock 控件上使用反射(它当然知道字符串是如何换行的)。如果你没有使用 MVVM,我想它适合你。

首先,我创建了一个最小的 window 来测试我的解决方案:

<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>

现在让我们看看代码隐藏中最重要的部分:

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

GetLineMetrics方法获取了LineMetrics的集合(一个内部对象,所以我不能直接使用它)。该对象有一个名为 "Length" 的 属性,其中包含您需要的信息。所以 GetLength 方法只是读取这个 属性 的值。

行存储在名为 tokens 的列表中,并使用 TextBox 控件显示(只是为了获得即时反馈)。

希望我的示例可以帮助您完成任务。

如果没有 public 方法来做到这一点,我会感到惊讶(尽管人们永远不知道,尤其是使用 WPF)。
确实看起来像 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);
    }
}

这里就不多解释了
获取该行的起始位置,减去上一行的起始位置以获得该行文本的长度,这就是我们。
唯一棘手(或不明显)的部分是需要将 ContentStart 偏移一个,因为设计 The TextPointer returned by this property always has its LogicalDirection set to Backward.,所以我们需要获取相同(!?)位置的指针,但使用 LogicalDirection set to Forward,不管这意味着什么。