CodeWF.Markdown:一個基於 Avalonia 12 的 Markdown 渲染控制項

CodeWF.Markdown:一個基於 Avalonia 12 的 Markdown 渲染控制項

這篇文章介紹 CodeWF.Markdown 的儲存庫位址、NuGet 安裝方式、完整版線、Lite 版線、即時編輯預覽、排版主題、程式碼高亮、圖片預覽、數學公式、多檢視器覆蓋和增量渲染能力。

最後更新 2026/5/16 下午9:47
dotnet9
預計閱讀 16 分鐘
分類
.NET Avalonia 桌面開發
專題
Avalonia
標籤
C# Avalonia Markdown MarkdownViewer 控制項 CodeWF

今天這篇文章,站長來聊聊我最近基本開發完成的 CodeWF.Markdown

這是一個基於 C# + Avalonia 12 + Markdig 做的 Markdown 渲染控制項。它最早來自 CodeWF.AvaloniaControls,後來我把 Markdown 相關程式碼單獨拆成了一個倉庫和一組 NuGet 套件:渲染控制項、主題資源、範例程式、測試都圍繞 Markdown 預覽這件事來組織。

它的定位不是「把 Markdown 轉成 HTML 後塞進 WebView」,而是把 Markdown AST 渲染成 Avalonia 控制項樹。這樣做的好處是:主題、字型、選取、複製、圖片預覽、程式碼區塊工具列、增量重新整理這些桌面控制項能力,都可以留在 Avalonia 體系裡處理。

這次文章不只放幾張介面圖,我把範例程式也跑起來重新截了一輪素材,包括靜態截圖和 GIF 動畫。先看一段功能巡覽。

為什麼單獨拆一個 Markdown 控制項

我平時寫文章、整理文件、做工具箱頁面時,經常需要一個本機 Markdown 預覽控制項。WebView 能做,但它會把問題帶到另一套體系裡:HTML/CSS 注入、指令碼安全、平台差異、複製行為、圖片預覽視窗、主題資源同步,都要額外處理。

Avalonia 桌面應用裡更自然的方式,是讓 Markdown 直接變成控制項樹:

  • 段落、標題、列表、引用、表格是 Avalonia 控制項。
  • 程式碼區塊可以掛複製按鈕、語言標識和醒目提示控制項。
  • 圖片可以用 Avalonia 的視窗能力做預覽、縮放、旋轉和另存。
  • 主題可以走 ResourceDictionary,而不是再維護一套 CSS。
  • 多個 MarkdownViewer 可以在同一個視窗裡擁有不同排版密度。

這就是 CodeWF.Markdown 的主要方向:做一個適合 Avalonia 應用直接嵌入的 Markdown Viewer,而不是再包一層瀏覽器。

當前套件線

目前倉庫裡主要有這幾條套件線:

  • CodeWF.Markdown:完整版 MarkdownViewer,包含程式碼醒目提示、圖片/SVG、數學公式、多語言資源、增量渲染等能力。
  • CodeWF.Markdown.Themes:完整版控制項範本和排版主題資源。
  • CodeWF.Markdown.Lite:輕量版 Viewer,直接套件引用只保留 Avalonia 和 Markdig,適合只需要基礎 Markdown 預覽的場景。
  • CodeWF.Markdown.Lite.Themes:輕量版對應主題資源。
  • CodeWF.Markdown.Sample:完整版範例程式。
  • CodeWF.Markdown.Lite.Sample:輕量版範例程式。
  • tests/CodeWF.Markdown.Tests:渲染模型、差異服務和主題資源相關測試。

倉庫位址在這裡:https://github.com/dotnet9/CodeWF.Markdown

當前截圖對應的倉庫版本是 12.0.3.1。我本機已經執行過:

dotnet build CodeWF.Markdown.slnx
dotnet test CodeWF.Markdown.slnx --no-restore

測試專案當前 13 個測試案例通過。建置時會看到 .NET 預覽版 SDK 的提示,但沒有編譯警告和錯誤。

範例程式:左邊編輯,右邊即時預覽

先看主範例程式。介面分成兩部分:左側是 Markdown 輸入,右側是渲染預覽。

上方工具列有幾個關鍵設定:

  • 應用主題:淺色、深色。
  • 排版主題:切換 Markdown 內容的強調色、標題邊線、引用背景、表格樣式、程式碼區樣式等。
  • 緊湊佈局:把字型、行高和區塊間距收緊,適合資訊密度更高的工具介面。
  • 語言:當前範例支援簡體中文、繁體中文、英文、日文。

左側下拉選單載入了多份 Markdown 範例檔案,用來覆蓋基礎元素、排版主題、程式碼與表格、列表引用、圖片連結、增量渲染壓力等場景。

這個範例程式不是為了做一個完整 Markdown 編輯器,而是為了驗證控制項在真實桌面視窗裡的表現:視窗縮放、捲動、主題切換、多語言、編輯區持續變更、預覽區增量重新整理,都要放在一起看。

常見 Markdown 元素

完整版使用 Markdig 解析 Markdown。常見元素基本都按控制項樹渲染:

  • 標題、段落、粗體、斜體、刪除線。
  • 行內程式碼和程式碼區塊。
  • 有序列表、無序列表、巢狀列表。
  • 任務列表。
  • 引用區塊,以及引用區塊裡的列表、程式碼和表格。
  • 表格、連結、分隔線。
  • 腳註、數學公式等延伸語法。
  • HTML inline 和 HTML block 的 fallback。

這裡有一個取捨:CodeWF.Markdown 不執行 HTML。也就是說,Markdown 裡寫 <section><span> 這類內容時,它會作為文字安全顯示出來,而不是像瀏覽器一樣執行 HTML 和樣式。對桌面預覽控制項來說,這個預設行為更穩,尤其適合展示外部輸入或使用者編輯內容。

程式碼區塊:醒目提示、語言標籤和複製按鈕

程式碼區塊是 Markdown 預覽裡很容易被忽略、但實際體驗很關鍵的部分。

當前程式碼區塊會顯示語言標籤和複製按鈕,並使用 TextMateSharp 做語法醒目提示。截圖裡可以看到 JSON、C#、TypeScript、Shell 等程式碼區塊的展示效果。

我沒有把程式碼區塊只做成一個純文字框,因為實際用 Markdown 讀文件時經常需要複製程式碼;另外,不同主題下程式碼區背景、邊框、字型大小和按鈕尺寸也要一起跟著資源變化。

控制項裡還預留了 CodeBlockToolRender 事件,用來讓外部專案在程式碼區塊工具區繼續加自己的按鈕。比如有些應用可能要在程式碼區塊上加「執行」「複製為命令」「傳送到終端機」這類入口,這些都不應該寫死在 MarkdownViewer 內部。

表格和長內容

表格不是簡單地拼字串,而是按行、列、儲存格渲染。表頭背景、邊框、文字顏色都從主題資源讀取。

這個點看起來不起眼,但對中文文件很重要。很多 Markdown 文件裡會有較長的中文說明、英文識別碼、連結、版本號碼和路徑。如果表格不能自然換行,預覽區域就會很容易出現橫向撐爆。

範例裡專門放了對齊表格和複雜儲存格內容,用來觀察:

  • 表頭和內容區邊框是否穩定。
  • 長文字是否在儲存格內換行。
  • 行內程式碼、加粗文字和連結是否能在表格裡正常顯示。
  • 主題切換後表格樣式是否立即更新。

圖片、SVG 和預覽視窗

Markdown 裡的圖片也是一個獨立控制項 MarkdownImage,不是簡單地把圖片塞到文件裡就結束。

它支援本機圖片、URL 圖片、Data URI,以及 SVG 圖片。

圖片載入失敗時,會顯示 fallback 文字,不會讓整個 Markdown 文件渲染中斷。

點擊圖片後會開啟預覽視窗:

預覽視窗支援:

  • 縮小、放大。
  • 1:1 顯示。
  • 適應視窗。
  • 左旋、右旋。
  • 另存為。

SVG 圖片在預覽時會額外轉成點陣圖作為預覽輸入,避免原始 Viewer 清理圖片資源後預覽視窗也跟著失效。最近這輪也專門處理了圖片資源釋放問題:Markdown 被替換、控制項離開可視樹、圖片載入還沒完成時,都要取消未完成任務並釋放點陣圖。

數學公式和化學表達式

完整版裡接入了 Sylinko.CSharpMath.Avalonia,用於數學公式渲染。最近新增的內部 MarkdownMathView,主要是為了讓公式前景色能跟隨當前 Markdown 主題。

這類細節在淺色主題裡不明顯,一切換到深色主題就會暴露出來:普通文字已經變成淺色,但公式如果仍然使用固定黑色,就會看不清。把公式也納入 Markdown 主題體系,才能保證整篇文件的閱讀體驗一致。

範例文件裡還包含化學表達式處理,用於把部分 LaTeX 化學命令轉成更適合複製和純文字提取的內容。它不是要取代專業公式編輯器,而是讓常見技術文章裡的公式段落,在桌面預覽裡能正常顯示。

排版主題:不是只換一個顏色

排版主題是這次控制項裡我比較重視的一塊。它不是簡單改一個 Accent Color,而是一整套 Markdown 閱讀資源。

主題切換效果可以看這張 GIF:

當前主題資源裡包含這些方向:

  • Basic
  • 橙心
  • 墨黑
  • 彩紫
  • 嫩青
  • 綠意
  • 紅緋
  • 藍螢
  • 科技藍
  • 蘭青
  • 山吹
  • 前端之峰
  • 極客黑
  • 簡潔

下面是科技藍主題下的效果:

深色應用主題加極客黑排版主題,則更適合程式碼和技術筆記:

主題資源統一使用固定 Key,例如:

<SolidColorBrush x:Key="CodeWFMarkdownAccentBrush" Color="#0F766E" />
<SolidColorBrush x:Key="CodeWFMarkdownQuoteBackgroundBrush" Color="#ECFDF5" />
<x:Double x:Key="CodeWFMarkdownParagraphLineHeight">31</x:Double>

這樣外部專案要自訂主題時,不需要改控制項程式碼,只要覆蓋這些資源 Key 即可。預設主題包也提供了 Light/Dark 兩套資源,切換 Avalonia 應用主題後,同一個 Markdown 排版主題會載入對應的亮色或暗色資源。

單個 Viewer 可以單獨覆蓋主題

一個真實應用裡,可能不只一個 Markdown 預覽區。比如左邊是正文預覽,右邊是評審記錄;或者主文件用普通排版,旁邊摘要用緊湊排版。

所以 MarkdownViewer 現在支援單個控制項覆蓋:

  • TypographyTheme
  • TypographySize
<md:MarkdownViewer
    Markdown="{Binding Markdown}"
    TypographyTheme="Simple"
    TypographySize="Small" />

多 Viewer 範例就是為了驗證這個場景:

Viewer A 可以跟隨上方統一設定,Viewer B 可以自己使用「簡潔 + 緊湊」。局部設定會寫入當前 Viewer 的資源範圍,不會污染同級 Viewer。

這一點對桌面應用很實用。很多應用不是只有一個「文章閱讀頁」,而是把 Markdown 預覽嵌在設定說明、版本更新、AI 回覆、日誌解釋、文件詳情、比對檢視裡。不同區域的閱讀密度不同,不能只靠全域主題一把梭。

增量渲染:盡量只取代變更區域

如果每次輸入一個字元都完整重建整個 Markdown 文件,短文字沒問題,長文件就容易卡。

CodeWF.Markdown 裡做了一個增量渲染路徑:控制項會保留已經渲染的區塊,文字變化後先透過差異服務判斷變更範圍,再盡量只取代受影響的區塊。

範例程式裡有一個「開始增量演示」按鈕,會自動模擬三類變化:

  • 取代文件中一段內容。
  • 在正文中部插入中文片段。
  • 在文件尾部追加新的 Markdown 區塊。

效果如下:

這套邏輯不是為了炫技,而是為了貼近真實編輯場景。中文文件裡經常不是只改一個英文單詞,而是改一整句話、插入一段說明、追加一個表格或程式碼區塊。增量渲染要處理的是這些連續文字變更。

當然,它也不是不顧正確性地強行局部重新整理。如果變更範圍過大、結構影響太多,或者局部取代不適合繼續復用舊區塊,就會退回完整渲染。對預覽控制項來說,正確性仍然比「永遠局部重新整理」更重要。

選區和複製

MarkdownViewer 是唯讀預覽控制項,但唯讀不代表不能互動。

當前控制項支援:

  • 選擇渲染後的文字。
  • 複製選區。
  • 在空白處複製整篇渲染文字。
  • 程式碼區塊單獨複製。

這裡有一個細節:渲染文字不是簡單返回原始 Markdown。比如表格、列表、圖片 alt、公式、任務列表等內容,都需要提取成適合複製的純文字。倉庫裡的 MarkdownParser 和共享渲染模型就承擔了一部分這類工作。

做這個功能時我更關心的是「複製出來能不能用」。如果使用者只是想從 Markdown 預覽裡複製一段說明、一個表格或一個程式碼區塊,就不應該被 Markdown 標記符打擾。

Lite 套件:給基礎預覽留一條輕量路徑

完整版功能比較全,但依賴也會多一些:

  • TextMateSharp 用於程式碼醒目提示。
  • Svg.Controls.Skia.AvaloniaSvg.Skia 用於 SVG。
  • Sylinko.CSharpMath.Avalonia 用於數學公式。
  • Lang.Avalonia.Json 用於多語言資源。

有些專案只需要基礎 Markdown 預覽,不需要這些延伸能力。為這個場景,我又拆了 CodeWF.Markdown.Lite

Lite 版保留:

  • 標題、段落、列表、任務列表。
  • 引用、表格。
  • 點陣圖圖片。
  • 純文字程式碼區塊和複製按鈕。
  • 對應的 Lite 主題套件。

它的直接套件引用只保留 Avalonia 和 Markdig,適合對依賴體積比較敏感的專案。如果需要程式碼醒目提示、SVG、數學公式、圖片預覽視窗這些能力,就用完整版。

接入方式

如果使用完整版,安裝套件:

Install-Package CodeWF.Markdown.Themes

如果習慣使用 .NET CLI,也可以這樣加套件:

dotnet add package CodeWF.Markdown.Themes

對應 NuGet 位址:

如果只需要輕量版基礎預覽,則安裝 Lite 套件線:

dotnet add package CodeWF.Markdown.Lite.Themes

Lite 對應 NuGet 位址:

專案原始碼在 GitHub:https://github.com/dotnet9/CodeWF.Markdown

App.axaml 引入主題:

<Application
    xmlns="https://github.com/avaloniaui"
    xmlns:markdown="https://codewf.com">
    <Application.Styles>
        <FluentTheme />
        <markdown:MarkdownThemes />
    </Application.Styles>
</Application>

頁面裡直接使用 MarkdownViewer

<UserControl
    xmlns="https://github.com/avaloniaui"
    xmlns:md="https://codewf.com">
    <ScrollViewer
        HorizontalScrollBarVisibility="Disabled"
        VerticalScrollBarVisibility="Auto">
        <md:MarkdownViewer
            Markdown="{Binding Markdown}"
            TypographyTheme="Simple"
            TypographySize="Small" />
    </ScrollViewer>
</UserControl>

如果不指定 TypographyThemeTypographySize,預設是 Basic + Normal,也可以在 MarkdownThemes 上設定全域預設。

執行時切換資源可以呼叫:

MarkdownThemes.OverrideTypographyResources(
    app,
    MarkdownTypographyThemes.BlueGlow,
    MarkdownTypographySizes.Small);

也可以只覆蓋某個視窗或某個控制項範圍,避免影響全域應用樣式。

倉庫組織

當前倉庫結構大致是這樣:

src/
  CodeWF.Markdown
  CodeWF.Markdown.Themes
  CodeWF.Markdown.Lite
  CodeWF.Markdown.Lite.Themes
  CodeWF.Markdown.Sample
  CodeWF.Markdown.Lite.Sample
  CodeWF.Markdown.Shared
tests/
  CodeWF.Markdown.Tests

CodeWF.Markdown.Shared 裡放共享渲染模型、Markdown 解析、差異服務等程式碼。完整版、Lite 和測試都能複用這一層邏輯。

主題資源放在 CodeWF.Markdown.ThemesCodeWF.Markdown.Lite.Themes 裡。這樣控制項程式碼、預設範本、排版資源可以分別維護,外部專案也能選擇只引用自己需要的套件。

最近這輪做了什麼

從更新日誌看,最近幾版主要在補這些東西:

  • 新增 CodeWF.Markdown.Lite 和對應主題套件、範例應用。
  • MarkdownViewer 新增 TypographyThemeTypographySize,支援單個 Viewer 覆蓋。
  • MarkdownThemes 新增緊湊型排版資源。
  • 範例應用調整為 Tab 結構,新增多 Viewer 演示。
  • 多語言資源從 Resx 切換為 JSON,並隨 NuGet content files 散佈。
  • 改進圖片資源釋放和圖片預覽視窗資源持有。
  • 新增內部 MarkdownMathView,讓數學公式顏色跟隨當前主題。
  • 補充 Markdown、主題、SVG 及相關組件的裁剪保留設定,改善裁剪發佈相容性。

這些工作看起來不如「新增一個大功能」顯眼,但對控制項長期可用很重要。尤其是主題資源、圖片釋放、局部 Viewer 覆蓋、Lite 套件線,它們決定了這個控制項能不能被放進真實專案,而不是只能在範例裡好看。

還可以繼續打磨的地方

CodeWF.Markdown 現在已經能覆蓋我自己常用的 Markdown 預覽場景,但它仍然有繼續打磨空間:

  • 主題可以繼續增加,並統一更多邊角細節。
  • 長文件與複雜表格還可以繼續做壓力測試。
  • 程式碼醒目提示語言覆蓋可以繼續驗證。
  • 圖片預覽視窗的快速鍵和互動還可以更完整。
  • 文件範例可以補更多「如何在業務專案中接入」的片段。
  • Lite 和完整版的能力邊界需要在 README 裡寫得更直觀。
  • AOT、裁剪發佈和不同平台字型差異還可以繼續測。

我現在對這個控制項的目標比較明確:先把常見閱讀和預覽場景做穩,再逐步補進階能力。Markdown 控制項很容易越做越散,所以套件線、主題資源、共享渲染模型和測試要先立住。

最後

CodeWF.Markdown 是我從自己專案裡拆出來的 Avalonia Markdown 渲染控制項。它的價值不在於「我也能渲染幾個標題和列表」,而在於把 Markdown 預覽作為桌面控制項認真處理:

  • 不是 WebView,而是 Avalonia 控制項樹。
  • 不是單一樣式,而是排版主題資源。
  • 不是只能全域設定,而是支援單個 Viewer 覆蓋。
  • 不是每次輸入都完整重建,而是盡量走增量渲染。
  • 不是只做完整版,也給基礎預覽留了 Lite 套件線。

對桌面工具、文件管理、AI 回覆預覽、更新日誌展示、設定說明、開發者工具箱這類場景來說,一個可主題化、可複製、可嵌入、可長期維護的 MarkdownViewer 還是很有價值的。

後面站長會繼續把它用到自己的工具和文章工作流裡,一邊用一邊補真實場景裡會遇到的細節。比起做一個一次性的 Demo,我更想把它打磨成 Avalonia 專案裡可以直接拿來用的 Markdown 預覽控制項。

繼續探索

延伸閱讀

更多文章