在 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 單位的寬度
也就是文本的單行排版里面就是通過各個字符的設計時寬度計算是否可以在一行排列,如果可以那就采用此優化,不再進行復雜文本排版,進入渲染邏輯
更多渲染相關博客請看 渲染相關
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/325215.html
標籤:WPF
上一篇:C# 提取PDF中的表格
