在 Avalonia 中显示 Markdown,最简单的需求是把一段字符串渲染出来。但如果字符串来自 AI,一次只到达几个字,还会包含代码、表格、公式和图表,接入方式就需要多考虑一步:如何持续追加内容,以及如何组织越来越长的对话。
StreamMark 是面向这类场景的 Avalonia 原生 Markdown 渲染库,不需要 WebView。本文从安装 NuGet 开始,先完成单篇文档显示,再接入流式输出和可选扩展。
本文由 AI 辅助整理,并基于项目源码和发布包核对。使用截至 2026 年 10 月 19 日已发布的
0.1.0-alpha.22,对应 .NET 10 与 Avalonia 12.1.3。它仍是预览版本,建议固定版本再接入。下文的 C# 接入 API 已通过发布包编译验证;配图是 TKCode 中实际集成 StreamMark 的 Headless + Skia 渲染截图,用于展示效果,并非下面最小程序的运行截图。

图 1:原生界面中的混合 Markdown 内容。公式、语法高亮和图表分别需要对应扩展包。
1. 准备 Avalonia 项目
准备 .NET 10 SDK。如果还没有 Avalonia 项目,可以先安装模板,再创建一个基础桌面项目:
dotnet new install Avalonia.Templates --version 12.1.3
dotnet new avalonia -n StreamMarkStarter
cd StreamMarkStarter
已有项目可以跳过创建步骤。确认目标框架为 net10.0,Avalonia 相关包统一使用 12.1.3,包括 Avalonia.Desktop、Avalonia.Themes.Fluent 及模板中启用的其他 Avalonia 包。不要只升级其中一个包,留下混用的大版本。
StreamMark 当前面向桌面应用。这里的示例使用 Fluent 主题,不要求安装 AtomUI。
2. 从 NuGet 安装 StreamMark
只显示普通 Markdown,先安装这一个包即可:
dotnet add package TKCode.StreamMark.Avalonia --version 0.1.0-alpha.22
注意两个名称的区别:NuGet 包名带有 TKCode. 前缀,但程序集和 C# 命名空间仍是 StreamMark.*。例如代码中使用的是 using StreamMark.Avalonia;。
需要更丰富的内容时,再安装对应扩展:
dotnet add package TKCode.StreamMark.Highlighting.TextMate --version 0.1.0-alpha.22
dotnet add package TKCode.StreamMark.RaTeX --version 0.1.0-alpha.22
dotnet add package TKCode.StreamMark.Mermaid --version 0.1.0-alpha.22
| 包 | 用途 |
|---|---|
TKCode.StreamMark.Avalonia |
Markdown 控件、默认主题;自动依赖 Core |
TKCode.StreamMark.Highlighting.TextMate |
代码语法高亮 |
TKCode.StreamMark.RaTeX |
LaTeX 公式 |
TKCode.StreamMark.Mermaid |
Mermaid 图表 |
TKCode.StreamMark.Theme.AtomUI |
已使用 AtomUI 的宿主应用进行主题映射 |
这里显式指定预览版本,不依赖 NuGet 客户端默认是否搜索预发行版。所有 StreamMark 扩展建议使用相同版本。
3. 在 App 中注册主题
打开 App.axaml,保留模板原有的 Fluent 主题,并在后面加入 StreamMark 主题:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sm="clr-namespace:StreamMark.Avalonia.Themes;assembly=StreamMark.Avalonia"
x:Class="StreamMarkStarter.App"
RequestedThemeVariant="Dark">
<Application.Styles>
<FluentTheme />
<sm:StreamMarkTheme />
</Application.Styles>
</Application>
x:Class 请与自己的项目命名空间保持一致。也可以在 App.Initialize() 中用 C# 注册,但不要和 XAML 重复添加:
Styles.Add(new StreamMark.Avalonia.Themes.StreamMarkTheme());
这里设置深色仅为演示;可以改为 Light 或使用系统主题。Fluent 等宿主主题提供常规控件模板,StreamMark 主题提供 Markdown 内容使用的资源,两者职责不同。
4. 显示第一篇 Markdown
将模板的 MainWindow.axaml 简化为:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="StreamMarkStarter.MainWindow"
Title="StreamMark 入门"
Width="960" Height="720" />
然后修改 MainWindow.axaml.cs。这一版只依赖基础包:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using StreamMark.Avalonia;
using StreamMark.Avalonia.Extensions;
namespace StreamMarkStarter;
public partial class MainWindow : Window
{
private readonly MarkdownServices _services = new();
private readonly MarkdownView _view = new();
public MainWindow()
{
InitializeComponent();
// 先设置服务,再设置 Markdown。
MarkdownServices.SetServices(_view, _services);
_view.Markdown = """
# Hello StreamMark
这是运行在 **Avalonia 原生控件** 中的 Markdown。
- 支持标题、段落和列表
- 可以选择、复制文字
- 接下来接入流式输出
| 项目 | 状态 |
| --- | --- |
| NuGet 安装 | 完成 |
| Avalonia 接入 | 完成 |
""";
Content = new ScrollViewer
{
Content = new Border
{
MaxWidth = 820,
Padding = new Thickness(28),
HorizontalAlignment = HorizontalAlignment.Stretch,
Child = _view
}
};
}
}
运行:
dotnet run
此时可以看到标题、列表和表格。MarkdownView 的高度会随文档增长,因此单篇文档通常由外层 ScrollViewer 提供滚动;MaxWidth 是为了控制阅读宽度。
5. 加入代码高亮、公式和 Mermaid
安装扩展包后,还需要注册插件。把上面的 _services 字段替换为:
private readonly MarkdownServices _services = new()
{
Plugins =
[
new StreamMark.Highlighting.TextMate.TextMatePlugin(),
new StreamMark.RaTeX.RaTeXPlugin(),
new StreamMark.Mermaid.MermaidPlugin()
]
};
然后把 _view.Markdown 替换为下面的内容:
_view.Markdown = """
## 代码高亮
```csharp
public record Message(string Role, string Content);
```
## 数学公式
行内公式:$E = mc^2$
$$
mathrm{Attention}(Q,K,V)
= mathrm{softmax}left(frac{QK^T}{sqrt{d_k}}
ight)V
$$
## Mermaid 流程图
```mermaid
flowchart LR
A[网络文本增量] --> B[MarkdownStream]
B --> C[增量解析]
C --> D[Avalonia 原生界面]
```
""";
C# 原始字符串 """ 不需要把 LaTeX 的反斜杠再转义一遍。这里的公式是 Markdown 内容,交给 RaTeX 插件处理。

图 2:TKCode 中通过 StreamMark 显示的 Mermaid 图表。默认配色会随宿主的明暗主题变化。
几个接入细节:
- 插件应在服务第一次使用前配置好。不要在已经渲染文档后再往同一个服务的插件列表追加配置。
- 将服务设置在具体
MarkdownView上最容易理解;多个子控件共用配置时,也可以设置到 Window 或共同祖先上。 - 使用插件时,手动创建的流式文档、会话条目要使用同一个
services.Profile,避免视图启用了插件,但文档仍使用默认解析配置。
RaTeX 的 NuGet 包携带六个桌面 RID 对应的原生库(Windows、Linux、macOS,各含 x64/arm64)。正常 NuGet 接入不要求自己安装 Rust,也不要求手动编译原生库。只有从源码构建或定制原生部分时,才需要走仓库的原生构建流程。
6. 把静态显示改成流式输出
不要在每个网络分片到达时都执行 _view.Markdown += chunk。该属性适合加载完整文档;流式回答应创建一次 MarkdownStream,后续只追加新增文本。
在同一个 MainWindow 类中加入:
private async Task PlayDemoAsync(CancellationToken cancellationToken)
{
var stream = new MarkdownStream(profile: _services.Profile);
_view.Stream = stream;
string[] chunks =
[
"# 流式回答
",
"你好,",
"**StreamMark**!
",
"- 原生渲染
",
"- 增量更新
"
];
try
{
foreach (var chunk in chunks)
{
cancellationToken.ThrowIfCancellationRequested();
stream.Push(chunk);
await Task.Delay(80, cancellationToken);
}
}
finally
{
// 即使取消,也结束当前流,保留已经收到的内容。
stream.Complete();
}
}
为便于运行演示,在构造函数末尾添加以下代码,让窗口打开后自动播放一次;这是对真实网络返回的模拟,不会调用模型 API:
Opened += async (_, _) => await PlayDemoAsync(CancellationToken.None);
示例需要 System.Threading 和 System.Threading.Tasks 命名空间;常见 .NET 模板启用 ImplicitUsings 后已自动导入。正式应用可以换成按钮触发,并在事件处理器中处理取消和错误。
真实模型接入时,只需把 chunks 换成 SDK 返回的文本增量。一轮回答创建一个流,按顺序 Push(),结束后 Complete()。如果 SDK 返回的是累计全文,先计算新增后缀,不能把整篇内容重复 Push。
Push() 和 Complete() 可以从后台线程调用;设置 _view.Stream、添加控件和更新会话条目仍要在 UI 线程进行。示例从 Avalonia 窗口事件进入,因此控件赋值在 UI 线程。
默认存在平滑输出队列,因此调用 Complete() 不代表屏幕上的文字已经同步全部出现。需要知道平滑队列也结束时,可以监听 stream.Completed;需要立即展示当前积压时,可以调用 stream.Flush()。
7. 长对话使用 TranscriptView
MarkdownView 是单文档控件,本身不提供虚拟化。聊天记录越来越长时,建议用 TranscriptView,让它组织消息、虚拟化显示行,并处理跨消息选区和滚动。
下面的代码可放在窗口构造函数中,替换原来的单文档 ScrollViewer 内容设置:
var transcript = new StreamMark.Avalonia.Transcript.TranscriptView
{
MaxContentWidth = 820
};
MarkdownServices.SetServices(transcript, _services);
// 用户消息可以是任意 Avalonia 控件。
transcript.Entries.Add(
new StreamMark.Avalonia.Transcript.ContentEntry(
new TextBlock { Text = "请介绍 StreamMark", Margin = new Thickness(16) }));
// 历史消息:用匹配插件的 Profile 创建静态条目。
transcript.Entries.Add(
StreamMark.Avalonia.Transcript.MarkdownEntry.FromText(
"这是一条 **历史回答**。", _services.Profile));
// 新回答:创建一次,持续追加。
var reply = new MarkdownStream(profile: _services.Profile);
transcript.Entries.Add(
new StreamMark.Avalonia.Transcript.MarkdownEntry(reply));
Content = transcript;
reply.Push("## 新回答
");
reply.Push("下面可以接入真实模型的文本增量。");
reply.Complete();
不要再把 TranscriptView 放入一个让它无限增高的外层纵向 ScrollViewer;保持窗口或 Grid 给它有限的可视高度。需要主动跳到底部时调用 transcript.ScrollToEnd(),而不是每次收到分片都强制滚到底部,打断用户阅读历史。

图 3:TKCode 中的 StreamMark 内容展示。diff、任务列表和 details 可以放在同一篇 Markdown 中;截图里的任务状态是预置示例。
8. 主题、链接和图片的常见问题
文字颜色异常或背景不协调。 先确认注册了 StreamMarkTheme,并检查宿主的明暗主题。StreamMark 的 Markdown 资源使用 Md.* 键;需要整体设计系统映射时,再考虑主题包。已经使用 AtomUI 的应用可以安装 TKCode.StreamMark.Theme.AtomUI,在 StreamMark 默认主题与宿主 AtomUI 主题就绪后加入 AtomUIMarkdownTheme。普通 Fluent 项目不必为了 Markdown 额外引入 AtomUI。
明明安装了扩展,却没有渲染效果。 安装包、注册插件、创建匹配 Profile 的文档是三件事。先确认服务已经设置到正确控件,再确认 MarkdownStream(profile: _services.Profile) 或 MarkdownEntry.FromText(text, _services.Profile) 使用了同一套配置。
长文档滑动时控件越来越多。 单个 MarkdownView 不等于虚拟化会话。多轮聊天请改用 TranscriptView;虚拟化减少视图实例和布局开销,并不会消除历史字符串和消息模型本身占用的内存。
发布后公式无法加载原生库。 按实际平台指定 RID,例如 dotnet publish -c Release -r osx-arm64。检查发布输出包含对应平台的 RaTeX 原生资产,不要把 x64 与 arm64 混用。Linux 上还应检查系统依赖与发行版环境。
图片路径或链接行为不符合预期。 入门时先使用可访问的 HTTPS 图片地址,减少相对路径解析问题。链接由 StreamMark 的链接策略约束;默认允许的外部协议为 HTTP、HTTPS 和 mailto,应用可通过 LinkClicked 接入自己的打开方式,并处理 RequiresConfirmation。不要不加判断地把任意 URL 交给系统启动。
Markdown 里能否执行脚本。 StreamMark 支持一部分 HTML 和白名单组件,但不是完整浏览器。script、style、iframe 等未知元素不会执行,应按原始源码展示。网页 CSS、JavaScript 插件不能直接照搬到原生 Markdown 控件里。
9. 推荐的接入顺序
先让基础包和默认主题显示一段静态 Markdown;再注册自己真正需要的扩展;接着通过 MarkdownStream 对接网络文本增量;最后按会话长度选择 TranscriptView,补上取消、错误提示、链接处理和图片策略。
本文示例只覆盖渲染接入,不包含模型认证、工具执行或会话持久化。StreamMark 负责把内容呈现在原生界面中,Agent 或聊天应用的业务流程仍由宿主管理。
参考入口:
验证说明:本文的服务注册、MarkdownView、MarkdownStream、TranscriptView 和三类扩展插件 API,已使用 NuGet
0.1.0-alpha.22与 Avalonia12.1.3在 .NET 10 下编译检查,结果为 0 警告、0 错误。配图来自已有真实组件渲染;本文未对所有操作系统进行运行测试,也未进行性能对比测试。
