主頁 > .NET開發 > dotnet 讀 WPF 源代碼筆記 簡單聊聊文本布局換行邏輯

dotnet 讀 WPF 源代碼筆記 簡單聊聊文本布局換行邏輯

2021-10-20 06:02:48 .NET開發

在 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/325210.html

標籤:.NET技术

上一篇:(轉發)龍芯 .NET 團隊,做作業去適配 .Net Core 3.1 ,意義何在?

下一篇:PublishFolderCleaner 讓你的 dotnet 應用發布檔案夾更加整潔

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more