UWP:计算 RichTextBlock 中的文本高度会产生奇怪的结果
UWP: Compute text height in a RichTextBlock gives weird results
我需要一种可靠的方法来获取 RichTextBlock
中包含的文本的高度,甚至在实际绘制到场景之前。
使用普通的 Measure() 方法会产生一个奇怪的结果,正如在 MVCE 中看到的那样:https://github.com/cghersi/UWPExamples/tree/master/MeasureText(我想保持固定宽度,并测量最终高度,但是结果DesiredSize 与实际身高相差甚远!!)。
出于这个原因,我找到了一个粗略的方法(这里提到 ),我扩展了它以达到我的目的,我们使用一些 Win2D API 来计算内容高度。
问题是在某些情况下,此方法提供的高度小于预期高度。
- 有没有一种通用的方法来检索一个(正确的)高度
TextBlock,甚至在它被绘制到场景之前?
- 如果不是这样,我做错了什么?
这是我的代码(您也可以在这里找到 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
对象中指定这样的设置?
查看您的 MeasureText MVCE 代码,在 RichTextBlock 上调用 Measure() 的问题归结为这一行:
m_textBlock.Margin = new Thickness(200);
这将所有边的通用边距设置为 200,这意味着该元素至少需要左侧 200 宽度加上右侧 200 宽度,或 400 宽度。由于您的 Measure(300,infinite) 指定的可用宽度小于所需的最小宽度 400,因此 RichTextBlock 决定它能做的最好的事情是在每个字符处包装文本,从而产生巨大的 5740 像素高度(加上 200+200从边距算起的高度)。
如果您删除该行,RichTextBlock 将使用指定的 300 约束并将其所需高度正确测量为 90 像素,这就是它在屏幕上呈现的高度(如果您设置 Width=300 或以其他方式导致实际元素布局具有相同的约束)。
或者,由于您知道元素的宽度,您可以在其上设置 Width=300,然后它会使用该宽度进行测量。但是,由于设置的边距,高度将被扩展。
我假设您实际上并没有在您的真实应用程序中设置 Margin=200,而是设置了更小的值(例如 Margin=5)来说明当 RichTextBlock 在树中和绘图时您实际需要的边距。如果是这种情况,那么您可以:
- 使用 Width=300 方法进行测量并从 DesireSize.Height 中减去顶部 + 底部边距。
- 用 (300+margin.Left+margin.Right) 作为宽度进行测量,这样一旦从总可用尺寸中减去边距,文本可以使用的剩余宽度就是您预期的 300。您'仍然需要从 DesireSize.Height.
中减去顶部 + 底部边距
我需要一种可靠的方法来获取 RichTextBlock
中包含的文本的高度,甚至在实际绘制到场景之前。
使用普通的 Measure() 方法会产生一个奇怪的结果,正如在 MVCE 中看到的那样:https://github.com/cghersi/UWPExamples/tree/master/MeasureText(我想保持固定宽度,并测量最终高度,但是结果DesiredSize 与实际身高相差甚远!!)。
出于这个原因,我找到了一个粗略的方法(这里提到
问题是在某些情况下,此方法提供的高度小于预期高度。
- 有没有一种通用的方法来检索一个(正确的)高度 TextBlock,甚至在它被绘制到场景之前?
- 如果不是这样,我做错了什么?
这是我的代码(您也可以在这里找到 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
对象中指定这样的设置?
查看您的 MeasureText MVCE 代码,在 RichTextBlock 上调用 Measure() 的问题归结为这一行:
m_textBlock.Margin = new Thickness(200);
这将所有边的通用边距设置为 200,这意味着该元素至少需要左侧 200 宽度加上右侧 200 宽度,或 400 宽度。由于您的 Measure(300,infinite) 指定的可用宽度小于所需的最小宽度 400,因此 RichTextBlock 决定它能做的最好的事情是在每个字符处包装文本,从而产生巨大的 5740 像素高度(加上 200+200从边距算起的高度)。
如果您删除该行,RichTextBlock 将使用指定的 300 约束并将其所需高度正确测量为 90 像素,这就是它在屏幕上呈现的高度(如果您设置 Width=300 或以其他方式导致实际元素布局具有相同的约束)。
或者,由于您知道元素的宽度,您可以在其上设置 Width=300,然后它会使用该宽度进行测量。但是,由于设置的边距,高度将被扩展。
我假设您实际上并没有在您的真实应用程序中设置 Margin=200,而是设置了更小的值(例如 Margin=5)来说明当 RichTextBlock 在树中和绘图时您实际需要的边距。如果是这种情况,那么您可以:
- 使用 Width=300 方法进行测量并从 DesireSize.Height 中减去顶部 + 底部边距。
- 用 (300+margin.Left+margin.Right) 作为宽度进行测量,这样一旦从总可用尺寸中减去边距,文本可以使用的剩余宽度就是您预期的 300。您'仍然需要从 DesireSize.Height. 中减去顶部 + 底部边距