UWP: вычисление высоты текста в RichTextBlock дает странные результаты - PullRequest
1 голос
/ 23 апреля 2019

Мне нужен надежный метод, чтобы получить высоту текста, содержащегося в RichTextBlock, даже до того, как он будет фактически нарисован на сцене.

Использование обычного метода Measure () приводит к странному результату, как это видно в MVCE: https://github.com/cghersi/UWPExamples/tree/master/MeasureText (я хочу сохранить фиксированную ширину и измерить конечную высоту, но результат DesiredSize сильно отличается от фактической высоты !!).

По этой причине я нашел грубый метод (упомянутый здесь https://stackoverflow.com/a/45937298/919700),), который я расширил для своих целей, где мы используем некоторый Win2D API для вычисления высоты содержимого.

Проблема в том, что в некоторых случаях этот метод обеспечивает высоту, которая меньше ожидаемой.

  1. Существует ли общий способ получения (правильной) высоты TextBlock, еще до того, как он нарисован на сцене?
  2. Если это не так, что я делаю не так?

Вот мой код (который вы также можете найти здесь как MVCE: https://github.com/cghersi/UWPExamples/tree/master/RichText):

    public sealed partial class MainPage
    {
        public static readonly FontFamily FONT_FAMILY = new FontFamily("Assets/paltn.ttf#Palatino-Roman");
        public const int FONT_SIZE = 10;
        private readonly Dictionary<string, object> FONT = new Dictionary<string, object>
        {
            { AttrString.FONT_FAMILY_KEY, FONT_FAMILY },
            { AttrString.FONT_SIZE_KEY, FONT_SIZE },
            { AttrString.LINE_HEAD_INDENT_KEY, 10 },
            { AttrString.LINE_SPACING_KEY, 1.08 },
            { AttrString.FOREGROUND_COLOR_KEY, new SolidColorBrush(Colors.Black) }
        };

        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        private readonly RichTextBlock m_displayedText;

        public MainPage()
        {
            InitializeComponent();

            // create the text block:
            m_displayedText = new RichTextBlock
            {
                MaxLines = 0, //Let it use as many lines as it wants
                TextWrapping = TextWrapping.Wrap,
                AllowFocusOnInteraction = false,
                IsHitTestVisible = false,
                Width = 80,
                Height = 30,
                Margin = new Thickness(100)
            };

            // set the content with the right properties:
            AttrString content = new AttrString("Excerpt1 InkLink", FONT);
            SetRichText(m_displayedText, content);

            // add to the main panel:
            MainPanel.Children.Add(m_displayedText);

            // compute the text height: (this gives the wrong answer!!):
            double textH = GetRichTextHeight(content, (float)m_displayedText.Width);
            Console.WriteLine("text height: {0}", textH);
        }

        public static double GetRichTextHeight(AttrString text, float maxWidth)
        {
            if (text == null)
                return 0;

            CanvasDevice device = CanvasDevice.GetSharedDevice();
            double finalH = 0;
            foreach (AttributedToken textToken in text.Tokens)
            {
                CanvasTextFormat frmt = new CanvasTextFormat()
                {
                    Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
                    FontFamily = textToken.Get(AttrString.FONT_FAMILY_KEY, FONT_FAMILY).Source,
                    FontSize = textToken.Get(AttrString.FONT_SIZE_KEY, FONT_SIZE),
                    WordWrapping = CanvasWordWrapping.Wrap
                };
                CanvasTextLayout layout = new CanvasTextLayout(device, textToken.Text, frmt, maxWidth, 0f);
                finalH += layout.LayoutBounds.Height;
            }

            return finalH;

            //return textBlock.Blocks.Sum(block => block.LineHeight);
        }

        private static void SetRichText(RichTextBlock label, AttrString str)
        {
            if ((str == null) || (label == null))
                return;
            label.Blocks.Clear();
            foreach (AttributedToken token in str.Tokens)
            {
                Paragraph paragraph = new Paragraph()
                {
                    TextAlignment = token.Get(AttrString.TEXT_ALIGN_KEY, TextAlignment.Left),
                    TextIndent = token.Get(AttrString.LINE_HEAD_INDENT_KEY, 0),
                };
                double fontSize = token.Get(AttrString.FONT_SIZE_KEY, FONT_SIZE);
                double lineSpacing = token.Get(AttrString.LINE_SPACING_KEY, 1.0);
                paragraph.LineHeight = fontSize * lineSpacing;
                paragraph.LineStackingStrategy = LineStackingStrategy.BlockLineHeight;
                Run run = new Run
                {
                    Text = token.Text,
                    FontFamily = token.Get(AttrString.FONT_FAMILY_KEY, FONT_FAMILY),
                    FontSize = fontSize,
                    Foreground = token.Get(AttrString.FOREGROUND_COLOR_KEY, new SolidColorBrush(Colors.Black)),
                    FontStyle = token.Get(AttrString.ITALIC_KEY, false) ? 
                        Windows.UI.Text.FontStyle.Italic : Windows.UI.Text.FontStyle.Normal
                };
                paragraph.Inlines.Add(run);
                label.Blocks.Add(paragraph);
            }
        }
    }

    public class AttrString
    {
        public const string FONT_FAMILY_KEY = "Fam";
        public const string FONT_SIZE_KEY = "Size";
        public const string LINE_HEAD_INDENT_KEY = "LhI";
        public const string LINE_SPACING_KEY = "LSpace";
        public const string FOREGROUND_COLOR_KEY = "Color";
        public const string ITALIC_KEY = "Ita";
        public const string TEXT_ALIGN_KEY = "Align";
        public const string LINE_BREAK_MODE_KEY = "LineBreak";

        public static Dictionary<string, object> DefaultCitationFont { get; set; }
        public static Dictionary<string, object> DefaultFont { get; set; }

        public List<AttributedToken> Tokens { get; set; }

        public AttrString(string text, Dictionary<string, object> attributes)
        {
            Tokens = new List<AttributedToken>();
            Append(text, attributes);
        }

        public AttrString(AttrString copy)
        {
            if (copy?.Tokens == null)
                return;
            Tokens = new List<AttributedToken>(copy.Tokens);
        }

        public AttrString Append(string text, Dictionary<string, object> attributes)
        {
            Tokens.Add(new AttributedToken(text, attributes));
            return this;
        }

        public bool IsEmpty()
        {
            foreach (AttributedToken t in Tokens)
            {
                if (!string.IsNullOrEmpty(t.Text))
                    return false;
            }

            return true;
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            foreach (AttributedToken t in Tokens)
            {
                sb.Append(t.Text);
            }
            return sb.ToString();
        }
    }

    public class AttributedToken
    {
        public string Text { get; set; }

        public Dictionary<string, object> Attributes { get; set; }

        public AttributedToken(string text, Dictionary<string, object> attributes)
        {
            Text = text;
            Attributes = attributes;
        }

        public T Get<T>(string key, T defaultValue)
        {
            if (string.IsNullOrEmpty(key) || (Attributes == null))
                return defaultValue;
            if (Attributes.ContainsKey(key))
                return (T)Attributes[key];
            else
                return defaultValue;
        }

        public override string ToString()
        {
            return Text;
        }
    }

** ОБНОВЛЕНИЕ **:

После дальнейшего изучения проблемы проблема, по-видимому, связана с отсутствием возможности конфигурирования для объекта CanvasTextFormat, особенно для отступа в первой строке (выражается в RichTextBlock с использованием свойства Paragraph.TextIndent). Есть ли способ указать такую ​​настройку в объекте CanvasTextFormat?

1 Ответ

1 голос
/ 22 мая 2019

Глядя на ваш код MeasureText MVCE, проблема с вызовом Measure () для RichTextBlock сводится к следующей строке:

    m_textBlock.Margin = new Thickness(200);

Это устанавливает универсальное поле 200 со всех сторон, что означает элементнужно как минимум 200 по ширине слева плюс 200 по ширине справа или 400 по ширине.Поскольку ваша мера (300, бесконечная) задает доступную ширину, которая меньше минимально необходимой ширины 400, RichTextBlock решает, что лучшее, что он может сделать, - это обтекание текста каждым символом, создавая массивную высоту 5740 пикселей (плюс 200 + 200высота от поля).

Если вы удалите эту строку, RichTextBlock будет использовать заданное ограничение 300 и правильно измерить его желаемую высоту как 90 пикселей, что и будет отображаться на экране (если вы установили Width= 300 или иным образом приведет к тому, что фактическая компоновка элемента будет иметь то же ограничение).

В качестве альтернативы, поскольку вы знаете ширину, которую вы хотите для элемента, вы можете установить для него ширину = 300, и она будет измеряться с помощьюэта ширина.Высота будет увеличена в результате установки Margin.

Я предполагаю, что на самом деле Margin = 200 не установлен в вашем реальном приложении, а вместо этого есть что-то меньшее, например Margin = 5 доучитывать маржу, которую вы на самом деле хотите, когда RichTextBlock находится в дереве и рисования.В этом случае вы можете:

  1. Использовать подход Width = 300 для измерения и вычитать верхнее + нижнее поле из DesireSize.Height.
  2. Измерять с помощью(300 + margin.Left + margin.Right) в качестве ширины, чтобы после того, как поле вычтено из общей доступной суммы. Размер оставшейся ширины, который может использовать текст, - это ваши предполагаемые 300. Вам все равно нужно будет вычесть верх + низполе от DesireSize.Height.
...