【问题标题】:Get the lines of the TextBlock according to the TextWrapping property?根据TextWrapping属性获取TextBlock的行数?
【发布时间】:2019-02-14 17:27:16
【问题描述】:

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

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

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

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

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

有什么想法吗?

【问题讨论】:

  • 你也可以显示你的代码吗?
  • @Hakam,我说得对,你想计算你的代码传播的行数,对吧?
  • @EmmanuelDURIN 不,我不想知道行数,我想知道每行的内容。对于每一行,我想知道行开始的字符,以及行结束的字符。换句话说,我想在对其应用 TextWrapping 算法后得到文本的结果
  • @Hakam,我花了一些时间看TextBlock的代码,认为可能有一个简单的解决方案。如果您可以读取私有属性,则计算行数非常容易。但是为了拥有 TextBlock 的内容,您需要访问几个(10 个?)私有/受保护/内部成员和一些内部/私有类。因此,编写自己的组件、绘制自己格式化的文本可能会更容易——控制每一行的内容。有可能的。 .Net中有这样一个类。有兴趣的告诉我
  • @EmmanuelDURIN 这门课到底能给我什么?你能解释一下这个类提供什么服务吗?

标签: c# wpf textblock word-wrap


【解决方案1】:

如果没有公开的方式来做到这一点,我会感到惊讶(尽管永远不知道,尤其是使用 WPF)。
确实看起来TextPointer class 是我们的朋友,所以这里有一个基于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 偏移一个,因为设计The TextPointer returned by this property always has its LogicalDirection set to Backward.,所以我们需要获取相同的指针(!? ) 位置,但使用 LogicalDirection set to Forward,不管这意味着什么。

【讨论】:

  • 首先:感谢您的回答。第二:我简要测试了您的解决方案(不是那么深入,它对我有用。第三:@II Vic 的解决方案不会崩溃(至少它不会因我的测试而崩溃)
  • 我真的更喜欢不深入私人财产的解决方案,我很高兴有这样的解决方案。不得不说II Vic的解决方案不错,但是如果有不使用类私有成员的解决方案,那就更好了。
  • @HakamFostok 抱歉,我不想冒犯他,我的测试确实崩溃了(试图比较它们是否提供相同的结果)。但是,当我只运行他的代码时,它不会崩溃!感谢您指出这一点(尽管我应该知道这一点)。我研究了一下,情况就是这样——只需在他的 OnCalculateClick var contentStart = txt.ContentStart; 的开头添加以下行。很无辜,对吧?现在他的代码崩溃了!无论如何,我将从我的答案中删除那部分。
  • 我同意@HakamFostok:这肯定是最好的解决方案,也是最优雅的解决方案!它不使用反射,所以它比我的更可取!
【解决方案2】:

使用FormattedText 类,可以首先创建格式化文本并评估其大小,因此您知道第一步所占用的空间, 如果太长,则由您决定分成几行。

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

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>

【讨论】:

  • 所以我得自己写根据属性(如Width、Height、TextWrapping ...)分割文本的算法,对吧?
  • 你说得对。写一个接近 TextBlock 的类不是很有趣,但与深入调用 TextBlock 的多个(可能是 10 或 20 个)私有成员/类相比,我认为它更干净。 TextBlock 对于管理 Flow 非常强大,所以它很复杂。请注意 FormattedText 完成了计算文本大小的工作
【解决方案3】:

如果没有问题,您可以在 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 控件显示(只是为了获得即时反馈)。

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

【讨论】:

  • 非常感谢,我对此进行了测试,它确实很有魅力。再次感谢你。在接受答案并给你赏金之前,我只需要再做一些测试。
  • @HakamFostok,即使它非常适合您,也请不要急于获得赏金。它吸引人们。人们投票和/或提供更多帮助。您可以获得更好的答案(或只是更有用的信息),而答案作者可以获得更多的赞成票以及最后的赏金。
  • @Sinatr 感谢您的建议,是的,我会这样做,我不会急于颁奖,因为我真的想要更多信息,尽管这是这次最好的解决方案,它真的解决了我的问题,但我会将赏金的发放推迟到期限结束。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-13
  • 1970-01-01
  • 2011-07-23
  • 2020-05-14
相关资源
最近更新 更多