# AtomUI 字体系统 — 运行时字体切换 > 本文档详细描述 AtomUI 中运行时切换全局字体的机制、实现原理、使用方式,以及注意事项和限制。 --- ## 1. 概述 AtomUI 支持在应用运行过程中动态切换全局字体族,无需重启应用。这得益于 Avalonia 的 `ResourceDictionary` 动态资源解析机制 —— 当资源字典中的 `FontFamily` 值被修改时,所有绑定了 `{atom:SharedTokenResource FontFamily}` 的控件会自动更新字体显示。 --- ## 2. 字体在主题资源中的存储 ### 2.1 初始加载流程 在应用启动时,字体族 Token 经过以下流程加载到主题资源中: ``` 1. DesignToken.InitSeedTokenValues() └─► FontFamily = "fonts:AlibabaSans#Alibaba Sans, ..." (Seed Token 默认值) 2. Theme.Load() → Theme.BuildThemeResource() └─► ResourceDictionary[SharedTokenKind.FontFamily] = designToken.FontFamily 3. ThemeManager.ThemeLoaded 事件 └─► 如果调用了 WithDefaultFontFamily(),则覆盖: loadedTheme.ThemeResource[SharedTokenKind.FontFamily] = userFontFamily 4. ThemeManager.SetActiveTheme() └─► Resources.ThemeDictionaries[themeVariant] = theme.ThemeResource ``` 最终,`SharedTokenKind.FontFamily` 作为动态资源存在于当前活动主题的 `ResourceDictionary` 中。 ### 2.2 控件如何消费 控件主题通过 Avalonia 的动态资源机制消费字体 Token: ```xml ``` `{atom:SharedTokenResource FontFamily}` 等效于 `{DynamicResource SharedTokenKind.FontFamily}`,它会在资源值变化时自动更新。 --- ## 3. 启动时设置默认字体 ### 3.1 使用 WithDefaultFontFamily 最简单的方式是在 `UseAtomUI()` 回调中使用 `WithDefaultFontFamily()`: ```csharp this.UseAtomUI(builder => { builder.UseAlibabaSansFont(); builder.UseDesktopControls(); // 启动时设置默认字体 builder.WithDefaultFontFamily("fonts:AlibabaSans#Alibaba Sans, $Default"); }); ``` **API 签名:** ```csharp public interface IThemeManagerBuilder { /// /// 设置默认字体族(FontFamily 对象) /// void WithDefaultFontFamily(FontFamily fontFamily); /// /// 设置默认字体族(字符串格式,自动解析为 FontFamily) /// void WithDefaultFontFamily(string fontFamily); } ``` ### 3.2 内部实现 ```csharp // ThemeManagerBuilder.cs public void WithDefaultFontFamily(FontFamily fontFamily) { FontFamily = fontFamily; } public void WithDefaultFontFamily(string fontFamily) { FontFamily = FontFamily.Parse(fontFamily); } ``` 设置后,在 `ApplicationExtensions.UseAtomUI()` 中注册 `ThemeLoaded` 事件处理器: ```csharp // ApplicationExtensions.cs var defaultFontFamily = themeManagerBuilder.FontFamily; themeManager.ThemeLoaded += (sender, args) => { if (defaultFontFamily != null && args.Theme != null) { var loadedTheme = args.Theme; loadedTheme.ThemeResource[SharedTokenKind.FontFamily] = defaultFontFamily; } }; ``` --- ## 4. 运行时动态切换字体 ### 4.1 通过修改主题资源字典 运行时切换字体的核心方法是直接修改当前活动主题的 `ResourceDictionary`: ```csharp using AtomUI.Theme; using AtomUI.Theme.Styling; using Avalonia; using Avalonia.Media; // 获取 ThemeManager 实例 var themeManager = AvaloniaLocator.Current.GetRequiredService(); // 获取当前活动主题的资源字典 if (themeManager.ActivatedTheme is { } activatedTheme) { // 修改 FontFamily Token 值 activatedTheme.ThemeResource[SharedTokenKind.FontFamily] = FontFamily.Parse("fonts:CustomFont#My Custom Font, $Default"); } ``` 修改后,所有使用 `{atom:SharedTokenResource FontFamily}` 绑定的控件会立即更新字体显示。 ### 4.2 封装为辅助方法 推荐封装一个辅助方法: ```csharp public static class FontSwitcher { /// /// 运行时切换全局字体族 /// /// 新的字体族(含回退链) public static void SwitchGlobalFont(string fontFamily) { SwitchGlobalFont(FontFamily.Parse(fontFamily)); } /// /// 运行时切换全局字体族 /// /// 新的字体族 public static void SwitchGlobalFont(FontFamily fontFamily) { var themeManager = AvaloniaLocator.Current.GetRequiredService(); if (themeManager.ActivatedTheme is { } activatedTheme) { activatedTheme.ThemeResource[SharedTokenKind.FontFamily] = fontFamily; } } } ``` **使用示例:** ```csharp // 在 ViewModel 或事件处理器中 FontSwitcher.SwitchGlobalFont("fonts:AlibabaSans#Alibaba Sans, $Default"); // 或切换到系统字体 FontSwitcher.SwitchGlobalFont("Segoe UI, PingFang SC, $Default"); ``` ### 4.3 在 UI 中提供字体选择 ```csharp // 示例:字体选择下拉框的事件处理 private void OnFontSelectionChanged(object sender, SelectionChangedEventArgs e) { var selectedFont = (e.AddedItems[0] as FontOption)?.FontFamilyString; if (!string.IsNullOrEmpty(selectedFont)) { FontSwitcher.SwitchGlobalFont(selectedFont); } } ``` --- ## 5. 字体切换与主题切换的关系 ### 5.1 主题切换时字体的行为 当用户切换主题(如从亮色切换到暗色)时: 1. `ThemeManager` 加载新主题的 `ResourceDictionary`。 2. 新主题的 `FontFamily` Token 恢复为 `DesignToken.InitSeedTokenValues()` 中的默认值。 3. `ThemeLoaded` 事件触发 — 如果之前调用了 `WithDefaultFontFamily()`,会重新覆盖字体值。 **重要:** 通过 `WithDefaultFontFamily()` 设置的字体在主题切换后**会保留**(因为它注册在 `ThemeLoaded` 事件上,每次主题加载都会执行)。但通过直接修改 `ResourceDictionary` 的运行时切换**不会跨主题保留** — 切换主题后需要重新设置。 ### 5.2 确保运行时字体切换跨主题保留 如果需要运行时字体切换在主题切换后依然有效,应当监听 `ThemeLoaded` 事件: ```csharp var themeManager = AvaloniaLocator.Current.GetRequiredService(); FontFamily? currentFontOverride = null; themeManager.ThemeLoaded += (sender, args) => { if (currentFontOverride != null && args.Theme != null) { args.Theme.ThemeResource[SharedTokenKind.FontFamily] = currentFontOverride; } }; // 切换字体时同时记录 public void SwitchFont(FontFamily fontFamily) { currentFontOverride = fontFamily; if (themeManager.ActivatedTheme is { } activatedTheme) { activatedTheme.ThemeResource[SharedTokenKind.FontFamily] = fontFamily; } } ``` --- ## 6. 注意事项与限制 ### 6.1 字体必须已注册 运行时切换到某个字体前,该字体必须已经通过 `FontManager.AddFontCollection()` 注册。否则 Avalonia 无法解析 `fonts:` URI,字体会回退到系统默认字体。 ```csharp // 确保字体包已注册 FontManager.Current.AddFontCollection(new CustomFontCollection()); // 然后才能切换 FontSwitcher.SwitchGlobalFont("fonts:CustomFont#My Custom Font, $Default"); ``` ### 6.2 字号梯度不受影响 运行时修改 `FontFamily` Token 只影响字体族,**不影响字号梯度**。字号梯度(`FontSizeSM`、`FontSizeLG`、`FontSizeHeading*` 等)是从 `FontSize` Seed Token 通过算法派生的,在主题加载时已经计算完成。 如果需要运行时修改基准字号,需要同时修改所有相关的 Map Token 值,或者触发主题重新加载。 ### 6.3 Popup/浮层控件 `PopupRoot` 和 `OverlayPopupHost` 控件的主题中同样绑定了 `{atom:SharedTokenResource FontFamily}`。这意味着运行时字体切换会自动影响所有浮层(如下拉菜单、工具提示、对话框等),无需额外处理。 ### 6.4 性能考虑 字体切换本质上是修改 `ResourceDictionary` 中的一个值,触发 Avalonia 的样式系统重新解析受影响控件的 `FontFamily` 属性。在控件树较大的应用中可能会有短暂的布局重算,但通常不构成性能瓶颈。 ### 6.5 回退链建议 始终在字体族字符串末尾包含 `$Default` 作为终极回退: ``` fonts:CustomFont#My Custom Font, Segoe UI, PingFang SC, $Default ``` 这确保即使自定义字体加载失败,应用仍然可以正常显示文本。 --- ## 7. 流程图:字体生命周期 ``` 应用启动 │ ▼ UseAtomUI(builder => { builder.UseAlibabaSansFont(); ──► FontManager 注册字体集合 builder.WithDefaultFontFamily(...); ──► 记录用户指定的字体族 }) │ ▼ ThemeManager.Configure() │ ▼ ThemeManager.LoadTheme() ├── Theme.BuildThemeResource() │ └── ResourceDict[FontFamily] = DesignToken.FontFamily (Seed 默认值) │ ├── ThemeLoaded 事件 │ └── ResourceDict[FontFamily] = 用户指定字体族 (WithDefaultFontFamily) │ └── 控件主题应用 └── {atom:SharedTokenResource FontFamily} → 从 ResourceDict 解析字体 │ ▼ 应用运行中... │ ├── 用户触发字体切换 │ └── activatedTheme.ThemeResource[FontFamily] = 新字体族 │ └── 所有绑定控件自动更新 ✅ │ ├── 用户触发主题切换(如亮色 → 暗色) │ ├── LoadTheme() 加载新主题 ResourceDict │ ├── ThemeLoaded 事件 → 重新应用 WithDefaultFontFamily ✅ │ └── 如果是运行时直接修改的 → 需要重新设置 ⚠️ │ ▼ 应用退出 ``` --- ## 8. 总结 | 场景 | 方法 | 跨主题保留 | |------|------|-----------| | 启动时设置默认字体 | `builder.WithDefaultFontFamily(...)` | ✅ 是 | | 运行时切换字体 | 修改 `ThemeResource[SharedTokenKind.FontFamily]` | ❌ 否(需监听 ThemeLoaded) | | 运行时切换字体(跨主题保留) | 监听 `ThemeLoaded` + 修改 `ThemeResource` | ✅ 是 |