Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MarkdView

License: MIT .NET Version

现代化 WPF Markdown 渲染控件,支持流式渲染、语法高亮和智能主题管理。

✨ 特性

  • 🚀 智能流式渲染 - 支持 AI 流式输出,自适应防抖优化(50ms-1000ms)
  • 🎨 语法高亮 - 内置多语言高亮支持
  • 😊 Emoji 支持 - 基于 Emoji.Wpf 的彩色 Emoji 渲染
  • 💻 Mac 风格代码块 - 带装饰性圆点的优雅代码展示
  • 🌓 智能主题管理 - 支持自动跟随全局主题或独立设置
  • 📐 比例字体缩放 - 所有文本元素随 FontSize 成比例缩放
  • 🔧 易扩展 - 基于 Markdig,支持丰富的 Markdown 特性
  • 高性能 - 重入保护、低优先级异步渲染,确保 UI 流畅
  • 📜 列表场景优化 - 支持在 ScrollViewer 中禁用内部滚动条

📦 安装

# 使用 NuGet 包管理器
Install-Package MarkdView

# 或使用 .NET CLI
dotnet add package MarkdView

🚀 快速开始

基础用法

<Window xmlns:markd="clr-namespace:MarkdView.Controls;assembly=MarkdView">
    <markd:MarkdownViewer Content="{Binding Content}" />
</Window>
public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _content = "# Hello MarkdView\n\nThis is **bold** text.";
}

主题管理

MarkdView 提供智能主题管理系统,基于全局静态变量 ThemeManager.CurrentTheme 作为唯一真实来源,支持两种使用模式:

模式 1:自动跟随全局主题(推荐)

不设置 Theme 属性(默认 ThemeMode.Auto),所有控件自动跟随全局主题:

<!-- 所有控件自动跟随全局主题 -->
<markd:MarkdownViewer Content="{Binding Content}" />
<!-- 或显式设置为 Auto -->
<markd:MarkdownViewer Content="{Binding Content}" Theme="Auto" />
using MarkdView;
using MarkdView.Enums;

// 在应用启动时初始化全局主题
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // 初始化全局主题(所有 Theme="Auto" 的控件都会使用此主题)
        ThemeManager.ApplyTheme(ThemeMode.Dark);
    }
}

// 运行时切换全局主题
ThemeManager.ApplyTheme(ThemeMode.Light);
ThemeManager.ApplyTheme(ThemeMode.Dark);

// 获取当前全局主题
var currentTheme = ThemeManager.CurrentTheme; // 始终返回当前实际使用的主题

使用场景

  • ✅ 应用中所有 Markdown 内容使用统一主题
  • ✅ 主题由应用级别统一管理(如跟随系统主题)
  • ✅ 简化主题管理逻辑

模式 2:独立主题设置

显式设置 Theme 属性为 LightDark,控件使用独立主题(并同步到全局):

<!-- 控件使用独立主题(数据绑定) -->
<markd:MarkdownViewer
    Content="{Binding Content}"
    Theme="{Binding Theme}" />

<!-- 或直接设置固定主题 -->
<markd:MarkdownViewer
    Content="{Binding Content}"
    Theme="Dark" />
public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private ThemeMode _theme = ThemeMode.Dark;

    [RelayCommand]
    private void SwitchToLight()
    {
        // 修改此属性会:
        // 1. 更新控件主题
        // 2. 同步更新 ThemeManager.CurrentTheme
        Theme = ThemeMode.Light;
    }

    [RelayCommand]
    private void SwitchToDark()
    {
        Theme = ThemeMode.Dark;
    }
}

使用场景

  • ✅ 不同 Markdown 内容需要使用不同主题
  • ✅ 主题切换由特定控件或 ViewModel 管理
  • ✅ 需要通过数据绑定动态切换主题

ThemeMode 枚举

public enum ThemeMode
{
    Auto = 0,   // 自动跟随全局主题(默认,推荐)
    Light = 1,  // 浅色主题
    Dark = 2    // 深色主题
}

主题同步机制与设计细节见 Guid.md

完整配置

<markd:MarkdownViewer
    Content="{Binding Content}"
    Theme="Auto"
    EnableStreaming="True"
    StreamingThrottle="50"
    EnableSyntaxHighlighting="True"
    UseTransparentCanvas="False"
    FontSize="12"
    FontFamily="Microsoft YaHei UI"
    VerticalScrollBarVisibility="Auto"
    HorizontalScrollBarVisibility="Auto" />

字体与字号设置

MarkdownViewer 支持直接设置 FontFamilyFontSize,也支持数据绑定。

<!-- 固定字体和字号 -->
<markd:MarkdownViewer
    Content="{Binding Content}"
    FontFamily="Microsoft YaHei UI"
    FontSize="14" />
<!-- 绑定到 ViewModel -->
<markd:MarkdownViewer
    Content="{Binding Content}"
    FontFamily="{Binding MarkdownFontFamily}"
    FontSize="{Binding MarkdownFontSize}" />

说明:

  • 修改 FontFamily / FontSize 后会立即重渲染并生效
  • FontSize 会按比例影响正文、标题、列表和代码块

透明画布开关

MarkdownViewer 提供 UseTransparentCanvas 属性,用于控制渲染画布是否透明:

  • False(默认):使用主题资源 Markdown.Background,保证主题一致性和可读性
  • True:将 FlowDocument 背景设为透明,适合嵌入已有卡片背景的场景
<!-- 默认行为:使用主题背景 -->
<markd:MarkdownViewer Content="{Binding Content}" UseTransparentCanvas="False" />

<!-- 透明画布:继承父容器视觉背景 -->
<markd:MarkdownViewer Content="{Binding Content}" UseTransparentCanvas="True" />

语法高亮开关

EnableSyntaxHighlighting 用于控制代码块是否启用语法高亮:

  • True(默认):代码块按语法类型着色
  • False:代码块以普通文本颜色显示
<!-- 启用语法高亮(默认) -->
<markd:MarkdownViewer Content="{Binding Content}" EnableSyntaxHighlighting="True" />

<!-- 关闭语法高亮 -->
<markd:MarkdownViewer Content="{Binding Content}" EnableSyntaxHighlighting="False" />

语法高亮配色(动态资源)

代码块语法色使用动态资源键 Markdown.Syntax.*。主题切换时会自动刷新,不需要手动重新创建控件。

你可以在 App.xaml 或运行时覆盖这些键:

<Application.Resources>
    <!-- 常用语法色覆盖示例 -->
    <SolidColorBrush x:Key="Markdown.Syntax.Default" Color="#1F2937" />
    <SolidColorBrush x:Key="Markdown.Syntax.Comment" Color="#6B7280" />
    <SolidColorBrush x:Key="Markdown.Syntax.String" Color="#B45309" />
    <SolidColorBrush x:Key="Markdown.Syntax.ControlKeyword" Color="#7C3AED" />
    <SolidColorBrush x:Key="Markdown.Syntax.Function" Color="#2563EB" />
</Application.Resources>

渲染完成事件

MarkdownViewer 提供 RenderCompleted 事件,用于监控 Markdown 内容何时完成渲染。这在需要显示加载动画、统计渲染性能或协调多个控件时非常有用。

基础用法

<markd:MarkdownViewer
    Content="{Binding Content}"
    RenderCompleted="OnMarkdownRenderCompleted" />
private void OnMarkdownRenderCompleted(object sender, EventArgs e)
{
    // Markdown 渲染完成,可以隐藏 loading 动画
    LoadingIndicator.Visibility = Visibility.Collapsed;
}

高级用法:通过 Attached Behavior 监控多个实例

在聊天应用等场景中,需要等待所有 AI 消息的 Markdown 都渲染完成后再显示内容:

1. 创建 Attached Behavior:

public static class MarkdownLoadedBehavior
{
    private static int _loadedCount = 0;
    private static int _totalCount = 0;
    private static Action? _onAllLoaded;

    // 开始跟踪
    public static void StartTracking(string sessionId, int totalCount, Action onAllLoaded)
    {
        _loadedCount = 0;
        _totalCount = totalCount;
        _onAllLoaded = onAllLoaded;
    }

    // Attached Property
    public static readonly DependencyProperty IsTrackingEnabledProperty =
        DependencyProperty.RegisterAttached(
            "IsTrackingEnabled",
            typeof(bool),
            typeof(MarkdownLoadedBehavior),
            new PropertyMetadata(false, OnIsTrackingEnabledChanged));

    public static bool GetIsTrackingEnabled(DependencyObject obj)
        => (bool)obj.GetValue(IsTrackingEnabledProperty);

    public static void SetIsTrackingEnabled(DependencyObject obj, bool value)
        => obj.SetValue(IsTrackingEnabledProperty, value);

    private static void OnIsTrackingEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MarkdownViewer markdownViewer && (bool)e.NewValue)
        {
            markdownViewer.RenderCompleted += OnMarkdownRenderCompleted;
        }
    }

    private static void OnMarkdownRenderCompleted(object? sender, EventArgs e)
    {
        _loadedCount++;

        if (_loadedCount >= _totalCount && _totalCount > 0)
        {
            _onAllLoaded?.Invoke();  // 所有渲染完成
            _loadedCount = 0;
            _totalCount = 0;
            _onAllLoaded = null;
        }
    }
}

2. 在 XAML 中使用:

<ItemsControl ItemsSource="{Binding Messages}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <markd:MarkdownViewer
                behaviors:MarkdownLoadedBehavior.IsTrackingEnabled="True"
                Content="{Binding Content}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

3. 在 ViewModel 中协调:

// 开始加载会话
IsLoadingMessages = true;

// 统计需要渲染的 Markdown 数量(例如:AI 消息数)
var markdownCount = session.Messages.Count(m => m.IsAIMessage);

// 开始跟踪渲染
MarkdownLoadedBehavior.StartTracking(
    sessionId,
    markdownCount,
    () =>
    {
        // 所有 Markdown 渲染完成后的回调
        Dispatcher.Invoke(() => IsLoadingMessages = false);
    }
);

事件触发时机

RenderCompleted 事件在以下情况触发:

  • Markdown 文本解析完成
  • FlowDocument 构建完成
  • 所有代码块语法高亮完成
  • 文档配置和布局完成

注意:事件在 DispatcherPriority.Background 优先级下触发,确保 UI 主线程不被阻塞。

列表场景使用

ScrollViewer 中使用多个 MarkdownViewer(如聊天消息列表),需要禁用内部滚动条以实现流畅的外层滚动体验:

<ScrollViewer VerticalScrollBarVisibility="Auto">
    <ItemsControl ItemsSource="{Binding Messages}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border Margin="10" Padding="15" Background="White">
                    <!-- 重要:必须禁用 MarkdownViewer 的滚动条 -->
                    <markd:MarkdownViewer
                        Content="{Binding Content}"
                        VerticalScrollBarVisibility="Disabled"
                        HorizontalScrollBarVisibility="Disabled" />
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

列表场景滚动行为说明

⚠️ 必须设置的属性

  • 必须将 VerticalScrollBarVisibility="Disabled" 设置在每个 MarkdownViewer
  • 必须将 HorizontalScrollBarVisibility="Disabled" 设置在每个 MarkdownViewer

🎯 滚动行为

  1. 外层文档滚动:鼠标滚轮事件会自动转发给外层 ScrollViewer,实现流畅的列表滚动
  2. 代码块滚动
    • 鼠标滚轮始终控制外层文档滚动(不会被代码块拦截)
    • 代码块内容只能通过拖动滚动条来滚动
    • 这样设计避免了滚动冲突,提供更好的用户体验
  3. 透明容器支持:即使外层 Border 背景设置为透明,滚动功能依然正常工作

💡 代码块操作提示

  • 复制代码:点击代码块右上角的复制按钮
  • 滚动代码:拖动代码块内的滚动条(不支持鼠标滚轮)
  • 文本选择:由于 WPF TextBlock 限制,暂不支持直接选中代码文本

🎨 主题定制

方式 1:运行时自定义(推荐)

在应用启动时加载主题并自定义颜色:

using MarkdView;
using MarkdView.Enums;

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // 应用主题
        ThemeManager.ApplyTheme(ThemeMode.Dark);

        // 自定义特定颜色
        Resources["Markdown.Heading.H1.Border"] = new SolidColorBrush(Color.FromRgb(0xFF, 0x69, 0xB4));
        Resources["Markdown.CodeBlock.Background"] = new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x1E));
    }
}

方式 2:在 App.xaml 中覆盖

<Application.Resources>
    <!-- 覆盖默认主题颜色 -->
    <SolidColorBrush x:Key="Markdown.Foreground" Color="#1E1E1E"/>
    <SolidColorBrush x:Key="Markdown.Heading.H1.Border" Color="#5C9DFF"/>
    <SolidColorBrush x:Key="Markdown.Quote.Background" Color="#F9F9F9"/>
    <SolidColorBrush x:Key="Markdown.CodeBlock.Background" Color="#282C34"/>
</Application.Resources>

可用主题资源键与颜色控制范围见 Guid.md

📝 支持的 Markdown 特性

基础语法

  • ✅ 标题 (H1-H6)
  • 粗体 / 斜体 / 删除线
  • ✅ 段落和换行
  • ✅ 引用块
  • ✅ 有序/无序列表
  • ✅ 链接和图片
  • ✅ 水平分隔线

高级特性

  • ✅ 代码块(Mac 风格设计 + 语法高亮)
  • 行内代码
  • ✅ 表格
  • ✅ 任务列表
  • ✅ Emoji 😊
  • ✅ GFM 扩展

语法高亮支持

C#, JavaScript, TypeScript, Python, Java, C/C++, Go, Rust, SQL, Bash, HTML, CSS, JSON, XML 等

📐 字体缩放系统

所有文本元素基于 FontSize 属性成比例缩放:

元素 缩放比例 示例(FontSize=12)
H1 标题 1.5× 18px
H2 标题 1.25× 15px
H3 标题 1.17× 14px
H4 标题 1.08× 13px
H5/H6 标题 1.0× 12px
正文 1.0× 12px
一级列表 1.08× 13px
嵌套列表 0.96× 11.5px
代码 0.92× 11px
<!-- 全局调整字体大小 -->
<markd:MarkdownViewer FontSize="14" Content="{Binding Content}" />

项目结构、性能优化与实现细节见 Guid.md

🛠️ 技术栈

  • .NET 8.0 - 现代化的 .NET 平台
  • WPF - Windows Presentation Foundation
  • Markdig 0.43.0 - 高性能 Markdown 解析器
  • Emoji.Wpf 0.3.4 - 彩色 Emoji 支持
  • CommunityToolkit.Mvvm - MVVM 工具包(示例项目)

🤝 贡献

欢迎提交 Issue 和 PR!

📄 许可证

MIT License - 详见 LICENSE 文件

🙏 致谢


Made with ❤️ for WPF developers

About

Markdown渲染器

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages