在 WPF 里面,带了基础的文本库功能,如 TextBlock 等。文本库排版的重点是在文本的分行逻辑,也就是换行逻辑,如何计算当前的文本字符串到达哪个字符就需要换到下一行的逻辑就是文本布局的重点模块。本文来简单聊聊 WPF 的文本布局逻辑

先写给不想阅读细节的大佬们了解 WPF 文本模块的布局逻辑: 文本的排版和渲染是分开的两个模块。 文本逻辑在排版里面,核心都会调用到 TextFormatterImp 里面,在这里将会通过 SimpleTextLine 尝试进行布局排版,在 SimpleTextLine 里面将会判断当前的文本字符串是否刚好一行能放下,如果可以放下,那么就使用当行方式显示。这是最为简单的,实现逻辑就是通过 Typeface 的 GlyphMetrics 的 AdvanceWidth 列表获取每个字符的排版宽度,将排版宽度乘以渲染字号即可获取每个字符占用的渲染布局宽度,将所有字符的占用布局框架之和 与可用行宽度进行比较,如果小于行宽度则进行单行布局

如果超过单行布局的能力,则进入 TextMetrics 的 FullTextLine 方法。此方法将使用到没有开源的 PresentationNative.dll 提供的 LoCreateLine 方法进行文本排版逻辑。在 PresentationNative 里面将会调用系统多语言处理 (也许是叫 TFS 但如果叫错了还请大佬们教教我)进行文本的复杂排版行为,包括进行合写字如蒙文藏文的排版逻辑。这部分复杂排版是需要系统层多语言的支持的,包含了复杂的语言文化规则

下面就是细节部分的逻辑

在 TextBlock 等的底层也是用到了 TextFormatterImp 的文本排版功能进行排版,然后进行渲染。渲染部分本文就不聊了

如在 TextBlock 的 OnRender 或 MeasureOverride 方法里面,都会调用 CreateLine 方法创建 Line 对象,接着通过 Line 对象的 Format 方法层层调用到 TextFormatterImp 里面,大概代码如下

    [ContentProperty("Inlines")]
[Localizability(LocalizationCategory.Text)]
public class TextBlock : FrameworkElement, IContentHost, IAddChildInternal, IServiceProvider
{
protected sealed override Size MeasureOverride(Size constraint)
{
// 忽略逻辑
// Create and format lines until end of paragraph is reached.
// Since we are disposing line object, it can be reused to format following lines.
Line line = CreateLine(lineProperties);
while (!endOfParagraph)
{
using(line)
{
// Format line. Set showParagraphEllipsis flag to false because we do not know whether or not the line will have
// paragraph ellipsis at this time. Since TextBlock is auto-sized we do not know the RenderSize until we finish Measure
line.Format(dcp, contentSize.Width, GetLineProperties(dcp == 0, lineProperties), textLineBreakIn, _textBlockCache._textRunCache, /*Show paragraph ellipsis*/ false); // 忽略其他逻辑
}
}
}
} // ----------------------------------------------------------------------
// Text line formatter.
// ----------------------------------------------------------------------
internal abstract class Line : TextSource, IDisposable
{
// ------------------------------------------------------------------
// Create and format text line.
//
// lineStartIndex - index of the first character in the line
// width - wrapping width of the line
// lineProperties - properties of the line
// textRunCache - run cache used by text formatter
// showParagraphEllipsis - true if paragraph ellipsis is shown
// at the end of the line
// ------------------------------------------------------------------
internal void Format(int dcp, double width, TextParagraphProperties lineProperties, TextLineBreak textLineBreak, TextRunCache textRunCache, bool showParagraphEllipsis)
{
// 忽略代码
_line = _owner.TextFormatter.FormatLine(this, dcp, width, lineProperties, textLineBreak, textRunCache);
}
} internal sealed class TextFormatterImp : TextFormatter
{
public override TextLine FormatLine(
TextSource textSource,
int firstCharIndex,
double paragraphWidth,
TextParagraphProperties paragraphProperties,
TextLineBreak previousLineBreak,
TextRunCache textRunCache
)
{
return FormatLineInternal(
textSource,
firstCharIndex,
0, // lineLength
paragraphWidth,
paragraphProperties,
previousLineBreak,
textRunCache
);
} /// <summary>
/// Format and produce a text line either with or without previously known
/// line break point.
/// </summary>
private TextLine FormatLineInternal(
TextSource textSource,
int firstCharIndex,
int lineLength,
double paragraphWidth,
TextParagraphProperties paragraphProperties,
TextLineBreak previousLineBreak,
TextRunCache textRunCache
)
{
// 忽略代码
}
}

通过上面代码可以看到在 WPF 框架,核心的文本排版逻辑是在 FormatLineInternal 方法里面

在 FormatLineInternal 里面将会先使用 SimpleTextLine 尝试作为一行进行布局,假设文本一行能放下,也就不需要复杂的排版逻辑,可以提升很大的性能。如果一行放不下,那就通过 TextMetrics 的 FullTextLine 进行复杂的排版逻辑

        /// <summary>
/// Format and produce a text line either with or without previously known
/// line break point.
/// </summary>
private TextLine FormatLineInternal(
TextSource textSource,
int firstCharIndex,
int lineLength,
double paragraphWidth,
TextParagraphProperties paragraphProperties,
TextLineBreak previousLineBreak,
TextRunCache textRunCache
)
{
// prepare formatting settings
FormatSettings settings = PrepareFormatSettings(/*忽略传入参数*/); TextLine textLine = null; if ( /*可以进行单行排版的文本*/ )
{
// simple text line.
textLine = SimpleTextLine.Create(/*忽略传入参数*/);
} if (textLine == null)
{
// content is complex, creating complex line
textLine = new TextMetrics.FullTextLine(/*忽略传入参数*/);
} return textLine;
}

在文本进行复杂排版,就需要用到没有开源的 PresentationNative.dll 提供的和系统层的多语言对接的功能。本文就仅来了解 SimpleTextLine 的实现

在 SimpleTextLine 里面,实现的逻辑是将当前的文本在传入的宽度内进行一行布局,如果能在一行进行布局,那就返回值,否则返回空

文本里面有段落和行和 TextRun 的三个概念,在开始了解 WPF 的代码之前,咱先定义这三个不同的概念。一个文本里面包含有多段,默认采用换行符作为分段。也就是说在一段里面是不会存在多个换行符的。一个段落里面将会因为文本框的宽度限制而存在多行。一行文本里面,将会因为文本属性的不同将文本分为多个 TextRun 对象

也就是最简单的文本就是一个字符,一个字符是一个 TextRun 放在一行里面,这一行放在一段里面

在 SimpleTextLine 的 Create 方法将层层调用进入到 CreateSimpleTextRun 方法里面,也就是说在一行里面将会一个个 TextRun 进行创建,创建的时候同时判断当前的文本剩余宽度是否足够

在 CreateSimpleTextRun 方法里面将会调用 Typeface.CheckFastPathNominalGlyphs 方法进行快速的创建,这个方法是没有开放出来给开发者使用的,调用这个方法可以绕过很多判断逻辑,性能很高

在 CheckFastPathNominalGlyphs 方法里面,将会使用 Typeface 的 TypefaceMetrics 属性作为 GlyphTypeface 类型的对象。此对象依然可以使用到没有开放给开发者使用的 GetGlyphMetricsOptimized 方法。如方法命名可以看到,这是一个有很多性能优化的方法。此方法将拿到文本字符串对应的 glyphIndices 和 glyphMetrics 两个数组,分别表示的是字符对应在 Glyph 的序号以及 Glyph 的信息,代码如下

            ushort[] glyphIndices = BufferCache.GetUShorts(charBufferRange.Length);
MS.Internal.Text.TextInterface.GlyphMetrics[] glyphMetrics = ignoreWidths ? null : BufferCache.GetGlyphMetrics(charBufferRange.Length); glyphTypeface.GetGlyphMetricsOptimized(charBufferRange,
emSize,
pixelsPerDip,
glyphIndices,
glyphMetrics,
textFormattingMode,
isSideways
);

以上的 glyphIndices 变量和 glyphMetrics 都是从 BufferCache 获取的,大部分排版逻辑都需要额外申请内存。此方法对比开放给开发者使用的版本的优势在于可以批量获取,给开发者使用的版本只能一个个字符获取,性能上远远不如调用此方法获取。更多关于开发者使用文本排版,请看 WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本

在拿到以上两个变量之后,即可进行计算每个字符的排版宽度,此计算方法将会让计算出来的值和实际渲染尺寸有一些误差。然而此排版方法只是计算是否在一行里面足够放下文本,有一些误差不会影响到结果。因为如果能一行进行排版,那就走以上的方法,是高性能模式。如果一行不能排版,那就通过系统层的语言文化进行排版,可以符合业务的需求

大概的计算逻辑如下

            //
// This block will advance until one of:
// 1. The end of the charBufferRange is reached
// 2. The charFlags have some of the charFlagsMask values
// 3. Glyph index is 0 (unless symbol font)
// 4. totalWidth > widthMax
// while(
i < charBufferRange.Length // charBufferRange 就是文本的 Char 列表
&& (ignoreWidths || totalWidth <= widthMax) // totalWidth 是当前文本已排版的字符的宽度之和
&& ((charFlags & charFlagsMask) == 0)
&& (glyph != 0 || symbolTypeface) // 在 glyph 是 0 时,表示的是当前没有字符,相当于 \0 字符。但是符号字体不在此范围
)
{
char ch = charBufferRange[i++];
if (ch == TextStore.CharLineFeed || ch == TextStore.CharCarriageReturn || (breakOnTabs && ch == TextStore.CharTab))
{
--i;
break;
}
else
{
int charClass = (int)Classification.GetUnicodeClassUTF16(ch);
charFlags = Classification.CharAttributeOf(charClass).Flags;
charFastTextCheck &= charFlags; glyph = glyphIndices[i-1];
if (!ignoreWidths)
{
totalWidth += TextFormatterImp.RoundDip(glyphMetrics[i - 1].AdvanceWidth * designToEm, pixelsPerDip, textFormattingMode) * scalingFactor;
}
}
}

上面逻辑核心就是 totalWidth <= widthMax 判断,判断当前布局的字符宽度之和是否小于可以使用的宽度。如果大于那就表示这一行放不下此字符串

计算单个字符占用的宽度使用的是 glyphMetrics[i - 1].AdvanceWidth * designToEm 进行计算,而 RoundDip 只是加上 Dpi 的辅助计算而已。以上的 AdvanceWidth 将是字符的宽度比例,可以乘以 designToEm 设计时的字号计算出 WPF 单位的宽度

也就是文本的单行排版里面就是通过各个字符的设计时宽度计算是否可以在一行排列,如果可以那就采用此优化,不再进行复杂文本排版,进入渲染逻辑

更多渲染相关博客请看 渲染相关

dotnet 读 WPF 源代码笔记 简单聊聊文本布局换行逻辑的更多相关文章

  1. dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标

    大家是否好奇,在 WPF 里面,对 UIElement 重写 OnRender 方法进行渲染的内容,是如何受到上层容器控件的布局而进行坐标偏移.如有两个放入到 StackPanel 的自定义 UIEl ...

  2. dotnet 读 WPF 源代码笔记 渲染收集是如何触发

    在 WPF 里面,渲染可以从架构上划分为两层.上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令.上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GF ...

  3. WPF学习笔记系列之一 (布局详情)

    布局:StackPanel  栈布局:控件不会拐弯且多出的不再显示.DockPanel   停靠布局 吸在上边下边或左右.WrapPanel    环绕布局   一行控件会拐弯Canvas  进行基于 ...

  4. 读Flask源代码学习Python--config原理

    读Flask源代码学习Python--config原理 个人学习笔记,水平有限.如果理解错误的地方,请大家指出来,谢谢!第一次写文章,发现好累--!. 起因   莫名其妙在第一份工作中使用了从来没有接 ...

  5. WPF自学笔记

    WPF使用哪几种元素作为顶级元素: 1. Window元素 2. Page元素(与Window元素类似,用于可导航的应用程序) 3. Application元素(定义应用程序资源和启动设置) PS:在 ...

  6. 《深入浅出WPF》笔记——绘画与动画

    <深入浅出WPF>笔记——绘画与动画   本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一 ...

  7. Prism for WPF 搭建一个简单的模块化开发框架(五)添加聊天、消息模块

    原文:Prism for WPF 搭建一个简单的模块化开发框架(五)添加聊天.消息模块 中秋节假期没事继续搞了搞 做了各聊天的模块,需要继续优化 第一步画页面 页面参考https://github.c ...

  8. 《深入浅出WPF》笔记——事件篇

    如果对事件一点都不了解或者是模棱两可的话,建议先去看张子阳的委托与事件的文章(比较长,或许看完了,也忘记看这一篇了,没事,我会原谅你的)http://www.cnblogs.com/JimmyZhan ...

  9. 《深入浅出WPF》笔记——资源篇

    原文:<深入浅出WPF>笔记--资源篇 前面的记录有的地方已经用到了资源,本文就来详细的记录一下WPF中的资源.我们平时的“资源”一词是指“资财之源”,是创造人类社会财富的源泉.在计算机程 ...

  10. 《深入浅出WPF》笔记——模板篇

    原文:<深入浅出WPF>笔记--模板篇 我们通常说的模板是用来参照的,同样在WPF中,模板是用来作为制作控件的参照. 一.认识模板 1.1WPF菜鸟看模板 前面的记录有提过,控件主要是算法 ...

随机推荐

  1. Android实际开发bug大总结

    目录介绍 1.1 java.lang.UnsatisfiedLinkError找不到so库异常 1.2 java.lang.IllegalStateException非法状态异常 1.3 androi ...

  2. 三维模型3DTile格式轻量化纹理压缩技术方法浅析

    三维模型3DTile格式轻量化纹理压缩技术方法浅析 三维模型的纹理数据通常占据了模型数据的大部分,因此纹理压缩对于3DTile格式轻量化压缩来说至关重要.下面将详细分析几种主要的纹理压缩技术方法: D ...

  3. Redis(5)——亿级数据过滤和布隆过滤器

    一.布隆过滤器简介 上一次 我们学会了使用 HyperLogLog 来对大数据进行一个估算,它非常有价值,可以解决很多精确度不高的统计需求.但是如果我们想知道某一个值是不是已经在 HyperLogLo ...

  4. KingbaseES 咨询锁

    传统的事务性锁,读/写会自动加锁,读/写完成后会自动解锁(加解锁机制在细节上复杂),这是一种隐式的锁机制.对于加锁后的并发控制,也就是默认的写不阻塞读,是通过MVCC机制解决的.这种锁完全不需要人为干 ...

  5. 【已解决】linux安装mysql依赖包(mysql-community-common-5.7.35-1.el7.x86_64)冲突

    错误信息: 软件包 mysql-community-common-5.7.35-1.el7.x86_64 (比 mysql-community-common-5.7.28-1.el7.x86_64 还 ...

  6. SSE与AVX指令基础介绍与使用

    SSE与AVX指令基础介绍与使用 SSE/AVX指令属于Intrinsics函数,由编译器在编译时直接在调用处插入代码,避免了函数调用的额外开销.但又与inline函数不同,Intrinsics函数的 ...

  7. postman测试接口时的参数对应接口的两种情况

    第一种通过json字符串的情况去进行测试,最终将json字符串转成对应的对象 代码里面一定要加上@RequestBody注解,即使是一个字符串也需要加这个注解 第二种通过key-value的形式去加入 ...

  8. #直径,线段树#51nod 1766 树上的最远点对

    题目 多组询问,在 \([a,b]\) 和 \([c,d]\) 中分别选一个点 \(x,y\) ,使得 \(dis(x,y)\) 最大 分析 考虑直径的一个性质,两个点集两条直径的四个端点可能成为合并 ...

  9. #cdq分治,树状数组#洛谷 4169 [Violet]天使玩偶/SJY摆棋子

    题目 分析 首先如果不会\(\text{K-DTree}\)的话,那就用CDQ分治吧 这题首先要去绝对值,分四种情况讨论, 只判断左下角的点 然后考虑怎样求最大值,这里采用树状数组,反正只是单点修改单 ...

  10. Java处理关键字进行脱敏操作

    1.通过表头获取需要处理的下标列 注:此处导出表格时对关键字进行脱敏处理 /** * . * 对表头进行过滤判断 * * @param headers 表头 * @return 对应的下标列及方法名 ...