天天看點

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 裡面,大概代碼如下

通過上面代碼可以看到在 WPF 架構,核心的文本排版邏輯是在 FormatLineInternal 方法裡面

在 FormatLineInternal 裡面将會先使用 SimpleTextLine 嘗試作為一行進行布局,假設文本一行能放下,也就不需要複雜的排版邏輯,可以提升很大的性能。如果一行放不下,那就通過 TextMetrics 的 FullTextLine 進行複雜的排版邏輯

在文本進行複雜排版,就需要用到沒有開源的 PresentationNative.dll 提供的和系統層的多語言對接的功能。本文就僅來了解 SimpleTextLine 的實作

在 SimpleTextLine 裡面,實作的邏輯是将目前的文本在傳入的寬度内進行一行布局,如果能在一行進行布局,那就傳回值,否則傳回空

文本裡面有段落和行和 TextRun 的三個概念,在開始了解 WPF 的代碼之前,咱先定義這三個不同的概念。一個文本裡面包含有多段,預設采用換行符作為分段。也就是說在一段裡面是不會存在多個換行符的。一個段落裡面将會因為文本框的寬度限制而存在多行。一行文本裡面,将會因為文本屬性的不同将文本分為多個 TextRun 對象

也就是最簡單的文本就是一個字元,一個字元是一個 TextRun 放在一行裡面,這一行放在一段裡面

在 SimpleTextLine 的 Create 方法将層層調用進入到 CreateSimpleTextRun 方法裡面,也就是說在一行裡面将會一個個 TextRun 進行建立,建立的時候同時判斷目前的文本剩餘寬度是否足夠

在 CreateSimpleTextRun 方法裡面将會調用 Typeface.CheckFastPathNominalGlyphs 方法進行快速的建立,這個方法是沒有開放出來給開發者使用的,調用這個方法可以繞過很多判斷邏輯,性能很高

在 CheckFastPathNominalGlyphs 方法裡面,将會使用 Typeface 的 TypefaceMetrics 屬性作為 GlyphTypeface 類型的對象。此對象依然可以使用到沒有開放給開發者使用的 GetGlyphMetricsOptimized 方法。如方法命名可以看到,這是一個有很多性能優化的方法。此方法将拿到文本字元串對應的 glyphIndices 和 glyphMetrics 兩個數組,分别表示的是字元對應在 Glyph 的序号以及 Glyph 的資訊,代碼如下

以上的 ​<code>​glyphIndices​</code>​ 變量和 ​<code>​glyphMetrics​</code>​ 都是從 BufferCache 擷取的,大部分排版邏輯都需要額外申請記憶體。此方法對比開放給開發者使用的版本的優勢在于可以批量擷取,給開發者使用的版本隻能一個個字元擷取,性能上遠遠不如調用此方法擷取。更多關于開發者使用文本排版,請看 ​​WPF 簡單聊聊如何使用 DrawGlyphRun 繪制文本​​

在拿到以上兩個變量之後,即可進行計算每個字元的排版寬度,此計算方法将會讓計算出來的值和實際渲染尺寸有一些誤差。然而此排版方法隻是計算是否在一行裡面足夠放下文本,有一些誤差不會影響到結果。因為如果能一行進行排版,那就走以上的方法,是高性能模式。如果一行不能排版,那就通過系統層的語言文化進行排版,可以符合業務的需求

大概的計算邏輯如下

上面邏輯核心就是 ​<code>​totalWidth &lt;= widthMax​</code>​ 判斷,判斷目前布局的字元寬度之和是否小于可以使用的寬度。如果大于那就表示這一行放不下此字元串

計算單個字元占用的寬度使用的是 ​<code>​glyphMetrics[i - 1].AdvanceWidth * designToEm​</code>​ 進行計算,而 RoundDip 隻是加上 Dpi 的輔助計算而已。以上的 AdvanceWidth 将是字元的寬度比例,可以乘以 designToEm 設計時的字号計算出 WPF 機關的寬度

也就是文本的單行排版裡面就是通過各個字元的設計時寬度計算是否可以在一行排列,如果可以那就采用此優化,不再進行複雜文本排版,進入渲染邏輯

更多渲染相關部落格請看 ​​渲染相關​​