根据TextWrapping属性获取TextBlock的行?

8
我在WPF应用程序中有一个TextBlock。
这个TextBlock的(Text, Width, Height, TextWrapping, FontSize, FontWeight, FontFamily)属性是动态的(在运行时由用户输入)。
每次用户更改前面提到的属性之一,TextBlock的Content属性就会在运行时更改。(直到这里一切正常)
现在,我需要根据之前指定的属性获取该TextBlock的行数。
也就是说,我需要TextWrapping算法产生的每一行分别放在单独的字符串中,或者我需要一个带有Scape Sequence \n的字符串。
您有什么好主意吗?

你能展示一下你的代码吗? - Salah Akbari
@Hakam,我没错吧,你想要计算你的代码分布在多少行上,对吧? - Emmanuel DURIN
@EmmanuelDURIN 不,我不想知道行数,我想知道每行的内容。对于每一行,我想知道行首和行尾的字符。换句话说,我想在应用TextWrapping算法后获取文本的结果。 - Hakan Fıstık
@Hakam,我花了一些时间观察TextBlock的代码,想着可能有一个简单的解决方案。如果你能读取一个私有属性,那么计算行数就很容易了。但是如果要获取TextBlock的内容,你需要访问几个(10个?)私有/受保护的/内部成员和一些少量的内部/私有类。所以也许编写自己的组件会更容易,绘制你自己格式化的文本 - 控制每行的内容。这是可能的。.Net中有这样一个类。如果你感兴趣,请告诉我。 - Emmanuel DURIN
@EmmanuelDURIN 这个类到底为我提供了什么?你能解释一下这个类提供的服务是什么吗? - Hakan Fıstık
3个回答

11

我会很惊讶如果没有公共方法实现这一点(虽然永远不知道,特别是对于WPF)。
事实上看起来TextPointer类是我们的好朋友,所以这里有一个基于TextBlock.ContentStartTextPointer.GetLineStartPositionTextPointer.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偏移一个位置,因为按设计此属性返回的TextPointer始终具有其LogicalDirection设置为Backward。因此,我们需要获取相同(!?)位置的指针,但其LogicalDirection设置为Forward,无论这意味着什么。


第一:感谢您的回答。第二:我简要测试了您的解决方案(没有深入测试),它对我有效。第三:@II Vic的解决方案没有崩溃(至少在我的测试中没有崩溃)。 - Hakan Fıstık
我真的更喜欢不需要深入私有属性的解决方案,很高兴有这样的解决方案。我必须说II Vic的解决方案很好,但如果有一种不使用类的私有成员的解决方案,那将会更好。 - Hakan Fıstık
抱歉,@HakamFostok。我并不想冒犯他,但在我的测试中,它真的会崩溃(试图比较它们是否提供相同的结果)。然而,当我只运行他的代码时,它不会崩溃!感谢你指出这一点(虽然我本应该知道)。我进行了调查,情况如下 - 只需在他的OnCalculateClick开头添加以下一行:var contentStart = txt.ContentStart;。这很无害,对吧?现在他的代码会崩溃!无论如何,我打算将这部分从我的答案中删除。 - Ivan Stoev
1
我同意@HakamFostok的观点:这肯定是最好的解决方案,也是最优雅的!它不使用反射,因此比我的更可取! - Il Vic

4

使用FormattedText类,可以先创建格式化文本并评估其大小,以便在第一步中确定它占用的空间, 如果太长,您可以将其拆分成单独的行。

然后在第二步中,可以绘制它。

所有操作都可以在以下方法的DrawingContext对象上完成:

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

这里是 自定义控件 解决方案:

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

那么我必须自己编写根据属性(如宽度、高度、文本换行等)拆分文本的算法,对吗? - Hakan Fıstık
你是对的。编写一个接近TextBlock的类并不是很有趣,但与挖掘调用TextBlock的多个(可能是10或20)私有成员/类相比,我认为它更加清晰。TextBlock在管理Flow方面非常强大,因此它也很复杂。请注意,FormattedText可以轻松计算文本大小。 - Emmanuel DURIN

2

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

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

<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控件显示(仅为了获得即时反馈)。
希望我的示例可以帮助您完成任务。

非常感谢,我测试了一下,它真的像魔法一样奏效。再次感谢您。在接受答案并给您悬赏之前,我只需要进行更多的测试。 - Hakan Fıstık
1
@HakamFostok,即使它对您完美地运作,请不要急于颁发赏金。这会吸引更多的人。人们会点赞和/或提供更多帮助。您可以获得更好的答案(或更有用的信息),而回答作者则可以获得更多的赞和最终的赏金。 - Sinatr
@Sinatr 感谢你的建议,我会按照你说的去做。我不会急于颁发奖励,因为我想要更多的信息。虽然这是目前为止最好的解决方案,确实解决了我的问题,但我会推迟奖励的颁发直到期限结束。 - Hakan Fıstık

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接