dotnet 读 WPF 源代码笔记 简单聊聊文本布局换行逻辑
在 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 源代码笔记 简单聊聊文本布局换行逻辑的更多相关文章
- dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标
大家是否好奇,在 WPF 里面,对 UIElement 重写 OnRender 方法进行渲染的内容,是如何受到上层容器控件的布局而进行坐标偏移.如有两个放入到 StackPanel 的自定义 UIEl ...
- dotnet 读 WPF 源代码笔记 渲染收集是如何触发
在 WPF 里面,渲染可以从架构上划分为两层.上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令.上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GF ...
- WPF学习笔记系列之一 (布局详情)
布局:StackPanel 栈布局:控件不会拐弯且多出的不再显示.DockPanel 停靠布局 吸在上边下边或左右.WrapPanel 环绕布局 一行控件会拐弯Canvas 进行基于 ...
- 读Flask源代码学习Python--config原理
读Flask源代码学习Python--config原理 个人学习笔记,水平有限.如果理解错误的地方,请大家指出来,谢谢!第一次写文章,发现好累--!. 起因 莫名其妙在第一份工作中使用了从来没有接 ...
- WPF自学笔记
WPF使用哪几种元素作为顶级元素: 1. Window元素 2. Page元素(与Window元素类似,用于可导航的应用程序) 3. Application元素(定义应用程序资源和启动设置) PS:在 ...
- 《深入浅出WPF》笔记——绘画与动画
<深入浅出WPF>笔记——绘画与动画 本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一 ...
- Prism for WPF 搭建一个简单的模块化开发框架(五)添加聊天、消息模块
原文:Prism for WPF 搭建一个简单的模块化开发框架(五)添加聊天.消息模块 中秋节假期没事继续搞了搞 做了各聊天的模块,需要继续优化 第一步画页面 页面参考https://github.c ...
- 《深入浅出WPF》笔记——事件篇
如果对事件一点都不了解或者是模棱两可的话,建议先去看张子阳的委托与事件的文章(比较长,或许看完了,也忘记看这一篇了,没事,我会原谅你的)http://www.cnblogs.com/JimmyZhan ...
- 《深入浅出WPF》笔记——资源篇
原文:<深入浅出WPF>笔记--资源篇 前面的记录有的地方已经用到了资源,本文就来详细的记录一下WPF中的资源.我们平时的“资源”一词是指“资财之源”,是创造人类社会财富的源泉.在计算机程 ...
- 《深入浅出WPF》笔记——模板篇
原文:<深入浅出WPF>笔记--模板篇 我们通常说的模板是用来参照的,同样在WPF中,模板是用来作为制作控件的参照. 一.认识模板 1.1WPF菜鸟看模板 前面的记录有提过,控件主要是算法 ...
随机推荐
- rnacos 版本更新为 v0.1.4
rnacos是一个用 rust重新实现的nacos. 周一发布 rnacos 后,有收到部分对2.0版本兼容问题的反馈. 主要是nacos2.0版本的注册心跳与1.0不同,rnacos之前没对2.0版 ...
- 不用写一行代码!Python最强自动化神器!
1.Playwright介绍 Playwright是一个由Microsoft开发的开源自动化测试工具,它可以用于测试Web应用程序.Playwright支持多种浏览器,包括Chrome.Firefox ...
- 靶场搭建----phpstudy2018安装及注意问题
安装 官网下载: https://www.xp.cn/download.html 新人推荐2018 版本phpstudy 介绍 系统服务------开机自启 非服务模式------开机不自启 搭建好环 ...
- CornerNet:经典keypoint-based方法,通过定位角点进行目标检测 | ECCV2018
论文提出了CornerNet,通过检测角点对的方式进行目标检测,与当前的SOTA检测模型有相当的性能.CornerNet借鉴人体姿态估计的方法,开创了目标检测领域的一个新框架,后面很多论文都基于Cor ...
- KingbaseES V8R6 集群中复制槽非活跃状态的可能原因
背景 此问题环境是一主五备物理集群,其中node1是主节点,node2,3是集群同步节点,node4,5是集群异地异步节点,由于异地和主节点不同网段,网速非常慢. kdts-plus工具纯迁数据,每分 ...
- ET介绍——Actor Location
Actor Location Actor模型只需要知道对方的InstanceId就能发送消息,十分方便,但是有时候我们可能无法知道对方的InstanceId,或者是一个Actor的InstanceId ...
- #树链剖分,LCA#洛谷 3398 仓鼠找sugar
题目 多次询问求树上的两条路径是否有公共点 分析 有公共点当且仅当一条路径的LCA在另一条路径上, 否则一定会形成一个环,那树剖求LCA判断一下LCA是否在另一条路径上即可 代码 #include & ...
- CentOS 9 x64 使用 Nginx、Supervisor 部署 Go/Golang 服务
前言 在 CentOS 9 x64 系统上,可以通过以下步骤来部署 Golang 服务. 1. 安装必要的软件包 安装以下软件包: Golang:Golang 编程语言 Nginx:Web 服务器 S ...
- Jetty使用入门
社区当前推荐开发者使用Jetty 12.X版本. 依据End of Community Support for Jetty 9.x - June 2022,社区对Jetty 9.x的支持,已在2022 ...
- 部署解压版mysql
1.检查系统是否安装过mysql //检查系统中有无安装过mysql rpm -qa|grep mysql //查询所有mysql 对应的文件夹,全部删除 whereis mysql find / - ...