520 lines
22 KiB
C#
520 lines
22 KiB
C#
using System.Reflection;
|
||
using System.Windows.Media.Animation;
|
||
using System.Collections;
|
||
|
||
namespace NeoUI.Appearance;
|
||
|
||
/// <summary>
|
||
/// 主题管理核心类:负责
|
||
/// 1. 维护当前明暗模式 + 调色板状态(_activeThemeMode/_activeThemePalette)
|
||
/// 2. 通过替换 Application.Resources.MergedDictionaries 中的主题/调色板 ResourceDictionary 实现运行时切换
|
||
/// 3. 可选颜色平滑动画(针对已有 SolidColorBrush 实例)
|
||
/// 4. 确保同一时刻仅存在一个主题字典 + 一个调色板字典,避免覆盖顺序错误
|
||
///
|
||
/// 重要概念:
|
||
/// - “主题字典” (Light.xaml / Dark.xaml) 定义基础背景 / 文本 / 语义色等
|
||
/// - “调色板字典” (LightBlue.xaml / DarkBlue.xaml 等) 定义品牌主色系(Primary*)
|
||
/// - WPF 资源解析为“后添加优先覆盖前面”,因此顺序必须:主题 -> 调色板 -> 其它控件样式
|
||
/// - 控件 XAML 需使用 DynamicResource 才能在字典替换时刷新;StaticResource 仅解析一次
|
||
/// </summary>
|
||
public class ThemeManager
|
||
{
|
||
/// <summary>当前库程序集名称,用于组合 pack:// 路径</summary>
|
||
public static string LibraryNamespace => Assembly.GetExecutingAssembly().GetName().Name;
|
||
|
||
/// <summary>主题资源根路径(基础 + 调色板都基于此)</summary>
|
||
public static string ThemesDictionaryPath => $"pack://application:,,,/{LibraryNamespace};component/Themes/";
|
||
|
||
/// <summary>内部缓存:当前明暗模式(避免重复加载)</summary>
|
||
private static ThemeMode _activeThemeMode = ThemeMode.Light;
|
||
/// <summary>内部缓存:当前调色板</summary>
|
||
private static ThemePalette _activeThemePalette = ThemePalette.Blue;
|
||
|
||
// 持久化开关:通过 EnablePersistence 控制是否写入文件
|
||
private static bool _persistenceEnabled;
|
||
|
||
/// <summary>主题模式变更事件(成功应用后才触发)</summary>
|
||
public static event Action<ThemeMode>? ThemeModeChanged;
|
||
/// <summary>主题调色板变更事件(成功应用后才触发)</summary>
|
||
public static event Action<ThemePalette>? ThemePaletteChanged;
|
||
|
||
#region Animation
|
||
/// <summary>
|
||
/// 对已有 SolidColorBrush 做颜色渐变动画。
|
||
/// 注意:它仅改变 Brush 实例的 Color,不负责替换资源字典;动画完成后再真正替换字典可保证:
|
||
/// - 已绑定此 Brush 的控件看到平滑过渡
|
||
/// - 替换字典后新增键/非 Brush 资源被更新
|
||
/// 限制:冻结 (Frozen) 的 Brush 无法动画(会直接跳过)
|
||
/// </summary>
|
||
private static Task AnimateBrushColorAsync(SolidColorBrush brush, Color toColor, int durationMs = 500)
|
||
{
|
||
if (brush.IsFrozen) return Task.CompletedTask; // 设计或某些场景可能出现冻结,如手动 Freeze()
|
||
|
||
var tcs = new TaskCompletionSource<bool>();
|
||
var anim = new ColorAnimation
|
||
{
|
||
To = toColor,
|
||
Duration = TimeSpan.FromMilliseconds(durationMs),
|
||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut },
|
||
FillBehavior = FillBehavior.HoldEnd // 保留最终值(防止回弹)
|
||
};
|
||
// Completed 总是回到 UI 线程上下文
|
||
anim.Completed += (_, _) => tcs.TrySetResult(true);
|
||
brush.BeginAnimation(SolidColorBrush.ColorProperty, anim);
|
||
return tcs.Task;
|
||
}
|
||
#endregion
|
||
|
||
#region Public Query
|
||
/// <summary>
|
||
/// 解析 Application 资源中当前主题字典(文件名包含 light.xaml / dark.xaml)以同步内部缓存。
|
||
/// 说明:外部调用可能绕过本类直接替换资源,因此这里以资源实际状态为准。
|
||
/// </summary>
|
||
public static ThemeMode GetAppThemeMode()
|
||
{
|
||
var dict = new ResourceDictionaryManager().LookupDictionary("themes");
|
||
if (dict?.Source != null)
|
||
{
|
||
var uri = dict.Source.ToString();
|
||
if (uri.IndexOf("light.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
_activeThemeMode = ThemeMode.Light;
|
||
else if (uri.IndexOf("dark.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
_activeThemeMode = ThemeMode.Dark;
|
||
}
|
||
return _activeThemeMode;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同上:根据当前调色板字典文件名更新内部缓存。
|
||
/// 注意:调色板文件名包含 Light/Dark 前缀 + 颜色名,例如 LightBlue.xaml / DarkBlue.xaml
|
||
/// </summary>
|
||
public static ThemePalette GetAppThemePalette()
|
||
{
|
||
var dict = new ResourceDictionaryManager().LookupDictionary("ColorPalette");
|
||
if (dict?.Source != null)
|
||
{
|
||
var uri = dict.Source.ToString();
|
||
if (uri.IndexOf("Blue.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
_activeThemePalette = ThemePalette.Blue;
|
||
else if (uri.IndexOf("Green.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
_activeThemePalette = ThemePalette.Green;
|
||
else if (uri.IndexOf("Purple.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
_activeThemePalette = ThemePalette.Purple;
|
||
}
|
||
return _activeThemePalette;
|
||
}
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 将当前 Application 级主题资源合并到某个单独窗口/控件的 Resources(用于隔离场景提前绑定)。
|
||
/// 通常不需要调用;只有在局部字典需要显式继承应用级主题时使用。
|
||
/// </summary>
|
||
public static void ApplyResourcesToElement(FrameworkElement frameworkElement)
|
||
{
|
||
if (frameworkElement == null) return;
|
||
var appMerged = Current.Resources.MergedDictionaries;
|
||
var target = frameworkElement.Resources.MergedDictionaries;
|
||
// 简单判断:数量少于应用就尝试补齐(不会去重深比较)
|
||
if (target.Count < appMerged.Count)
|
||
{
|
||
foreach (var d in appMerged)
|
||
if (!target.Contains(d))
|
||
target.Add(d);
|
||
}
|
||
}
|
||
|
||
#region Unified Theme API
|
||
/// <summary>
|
||
/// 异步统一入口:可同时改变明暗模式 + 调色板。只传 mode 或 palette 代表单独改变。
|
||
/// animate=true 启用颜色渐变(注意:仅对已经存在的 SolidColorBrush 有效果)。
|
||
/// </summary>
|
||
public static Task ApplyThemeAsync(ThemeMode? mode = null,
|
||
ThemePalette? palette = null,
|
||
bool animate = false,
|
||
int animationDurationMs = 350)
|
||
=> InternalApplyThemeAsync(mode, palette, animate, animationDurationMs);
|
||
|
||
/// <summary>
|
||
/// 同步封装(内部实际仍走异步,只是阻塞等待)。
|
||
/// UI 线程中调用时不建议长动画阻塞;如果需要平滑体验请直接使用 await ApplyThemeAsync。
|
||
/// </summary>
|
||
public static void ApplyTheme(ThemeMode? mode = null,
|
||
ThemePalette? palette = null,
|
||
bool animate = false,
|
||
int animationDurationMs = 350)
|
||
=> InternalApplyThemeAsync(mode, palette, animate, animationDurationMs).GetAwaiter().GetResult();
|
||
|
||
/// <summary>
|
||
/// 主题/调色板应用核心逻辑:
|
||
/// 1. 计算是否真的发生变化(避免重复加载)
|
||
/// 2. 预加载新字典(不合并)用于取目标颜色参与动画
|
||
/// 3. 可选对现有 Brush 做动画(不创建新实例,避免引用断开)
|
||
/// 4. 替换 ResourceDictionary(删除旧 -> 添加新,保证唯一顺序)
|
||
/// 5. 更新内部状态并触发事件
|
||
/// </summary>
|
||
private static async Task InternalApplyThemeAsync(ThemeMode? mode,
|
||
ThemePalette? palette,
|
||
bool animate,
|
||
int durationMs)
|
||
{
|
||
// Step1: 获取真实当前状态(可能被外部改动过)
|
||
var currentMode = GetAppThemeMode();
|
||
var currentPalette = GetAppThemePalette();
|
||
|
||
// Step2: 计算目标(未传则沿用现值)
|
||
var newMode = mode ?? currentMode;
|
||
var newPalette = palette ?? currentPalette;
|
||
|
||
// Step3: 判断变化
|
||
// 明暗模式改变时:对应调色板文件(LightBlue -> DarkBlue)必然需要重新加载,即便颜色名相同
|
||
bool modeChanged = newMode != currentMode;
|
||
bool paletteChanged = newPalette != currentPalette || modeChanged;
|
||
|
||
if (!modeChanged && !paletteChanged) return; // 无变化直接结束
|
||
|
||
// Step4: 组装文件名(当前命名约定:Dark/Light + 调色板名)
|
||
string modeName = newMode == ThemeMode.Dark ? "Dark" : "Light";
|
||
string paletteName = newPalette switch
|
||
{
|
||
ThemePalette.Blue => "Blue",
|
||
ThemePalette.Green => "Green",
|
||
ThemePalette.Purple => "Purple",
|
||
_ => "Blue" // 兜底,理论不应走到
|
||
};
|
||
|
||
// Step5: 构造 Uri(pack://)
|
||
var themeUri = new Uri($"{ThemesDictionaryPath}{modeName}.xaml", UriKind.Absolute);
|
||
var paletteUri = new Uri($"{ThemesDictionaryPath}ColorPalette/{modeName}{paletteName}.xaml", UriKind.Absolute);
|
||
|
||
// Step6: 预加载字典(此时资源已经解析,可取得目标 Brush 颜色)
|
||
ResourceDictionary? newThemeDict = null;
|
||
ResourceDictionary? newPaletteDict = null;
|
||
if (modeChanged) newThemeDict = new ResourceDictionary { Source = themeUri };
|
||
if (paletteChanged) newPaletteDict = new ResourceDictionary { Source = paletteUri };
|
||
|
||
// Step7: 动画(仅针对已有 Brush)
|
||
if (animate)
|
||
{
|
||
var tasks = new List<Task>();
|
||
if (newThemeDict != null) CollectBrushAnimations(newThemeDict, tasks, durationMs);
|
||
if (newPaletteDict != null) CollectBrushAnimations(newPaletteDict, tasks, durationMs);
|
||
if (tasks.Count > 0) await Task.WhenAll(tasks);
|
||
// 动画完成后,将已动画的旧 Brush 实例复用到新字典,避免字典替换产生新实例导致“跳变”
|
||
if (newThemeDict != null) ReuseAnimatedBrushInstances(newThemeDict);
|
||
if (newPaletteDict != null) ReuseAnimatedBrushInstances(newPaletteDict);
|
||
}
|
||
|
||
// Step8: 真正替换 / 添加字典(保证唯一 + 顺序)
|
||
if (modeChanged) ReplaceOrAddDictionary(themeUri, isTheme: true);
|
||
if (paletteChanged) ReplaceOrAddDictionary(paletteUri, isTheme: false);
|
||
|
||
// Step9: 更新内部状态 & 发事件(只在实际变更时触发)
|
||
if (modeChanged)
|
||
{
|
||
_activeThemeMode = newMode;
|
||
ThemeModeChanged?.Invoke(newMode);
|
||
}
|
||
if (paletteChanged)
|
||
{
|
||
_activeThemePalette = newPalette;
|
||
ThemePaletteChanged?.Invoke(newPalette);
|
||
}
|
||
|
||
// Step10: 可选持久化(只要实际有改变才写)
|
||
if (_persistenceEnabled && (modeChanged || paletteChanged))
|
||
{
|
||
try { ThemePreferenceStore.Save(_activeThemeMode, _activeThemePalette); } catch { /* 忽略 */ }
|
||
}
|
||
|
||
#if DEBUG
|
||
// 调试辅助:输出当前合并字典列表,便于确认顺序是否正确,以及是否重复遗留
|
||
DumpMerged("AfterApplyTheme");
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为动画阶段收集“可以做颜色过渡”的画刷任务:仅当新字典中存在相同 Key 且旧资源中对应是 SolidColorBrush。
|
||
/// 注意:这里不替换 Brush 实例,只修改 Color,保持绑定引用稳定。
|
||
/// </summary>
|
||
private static void CollectBrushAnimations(ResourceDictionary? newDict, List<Task> tasks, int durationMs)
|
||
{
|
||
var existing = Current.Resources;
|
||
foreach (DictionaryEntry entry in newDict)
|
||
{
|
||
if (entry.Value is SolidColorBrush newBrush && existing[entry.Key] is SolidColorBrush oldBrush && !ReferenceEquals(oldBrush, newBrush))
|
||
tasks.Add(AnimateBrushColorAsync(oldBrush, newBrush.Color, durationMs));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动画后复用已经过渡完成的旧 Brush 实例,避免替换字典时创建新实例导致闪烁或动画丢失。
|
||
/// 仅覆盖同 Key SolidColorBrush;其它资源仍使用新字典的对象(确保新增键/不同类型可生效)。
|
||
/// </summary>
|
||
private static void ReuseAnimatedBrushInstances(ResourceDictionary? newDict)
|
||
{
|
||
var existing = Current.Resources;
|
||
var keys = new List<object>();
|
||
if (newDict == null) return;
|
||
foreach (DictionaryEntry entry in newDict)
|
||
{
|
||
if (entry.Value is SolidColorBrush && existing[entry.Key] is SolidColorBrush existingBrush)
|
||
{
|
||
keys.Add(entry.Key);
|
||
}
|
||
}
|
||
|
||
foreach (var key in keys)
|
||
{
|
||
newDict[key] = existing[key]; // 复用经过动画的旧实例
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 核心替换策略:
|
||
/// 1. isTheme=true → 删除所有旧的 Light.xaml/Dark.xaml 后再插入新主题(插在所有调色板之前)
|
||
/// 2. isTheme=false → 删除所有旧调色板后,在主题后面插入新调色板
|
||
/// 保证:不会出现多个主题或多个调色板同时存在;顺序固定;避免旧的 Light 覆盖新 Dark。
|
||
/// </summary>
|
||
private static void ReplaceOrAddDictionary(Uri uri, bool isTheme)
|
||
{
|
||
var merged = Current.Resources.MergedDictionaries;
|
||
|
||
// 判定函数:主题字典(不包含 /ColorPalette/ 且文件名为 Light.xaml 或 Dark.xaml)
|
||
bool IsThemeDict(ResourceDictionary d)
|
||
{
|
||
var s = d.Source?.ToString();
|
||
return s != null &&
|
||
s.IndexOf("/Themes/", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||
s.IndexOf("/ColorPalette/", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||
(s.EndsWith("Light.xaml", StringComparison.OrdinalIgnoreCase) ||
|
||
s.EndsWith("Dark.xaml", StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
// 判定函数:调色板字典(路径包含 /Themes/ColorPalette/)
|
||
bool IsPaletteDict(ResourceDictionary d)
|
||
{
|
||
var s = d.Source?.ToString();
|
||
return s != null &&
|
||
s.IndexOf("/Themes/ColorPalette/", StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
|
||
if (isTheme)
|
||
{
|
||
// A. 先删除所有旧主题字典(倒序防止索引错位)
|
||
for (int i = merged.Count - 1; i >= 0; i--)
|
||
if (IsThemeDict(merged[i]))
|
||
merged.RemoveAt(i);
|
||
|
||
// B. 找到第一个调色板位置(主题应在其前面)
|
||
int firstPaletteIndex = -1;
|
||
for (int i = 0; i < merged.Count; i++)
|
||
if (IsPaletteDict(merged[i]))
|
||
{
|
||
firstPaletteIndex = i;
|
||
break;
|
||
}
|
||
|
||
// C. 插入新主题(在调色板前,保证调色板覆盖可能重复键)
|
||
var newTheme = new ResourceDictionary { Source = uri };
|
||
if (firstPaletteIndex >= 0)
|
||
merged.Insert(firstPaletteIndex, newTheme);
|
||
else
|
||
merged.Add(newTheme);
|
||
}
|
||
else
|
||
{
|
||
// 调色板替换流程:
|
||
// 1. 删除旧调色板
|
||
for (int i = merged.Count - 1; i >= 0; i--)
|
||
if (IsPaletteDict(merged[i]))
|
||
merged.RemoveAt(i);
|
||
|
||
// 2. 定位主题字典(插在它后面)
|
||
int themeIndex = -1;
|
||
for (int i = 0; i < merged.Count; i++)
|
||
if (IsThemeDict(merged[i]))
|
||
{
|
||
themeIndex = i;
|
||
break;
|
||
}
|
||
|
||
// 3. 插入新调色板
|
||
var newPalette = new ResourceDictionary { Source = uri };
|
||
if (themeIndex >= 0 && themeIndex + 1 <= merged.Count)
|
||
merged.Insert(themeIndex + 1, newPalette);
|
||
else
|
||
merged.Add(newPalette);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Backward Compatible APIs
|
||
/// <summary>
|
||
/// 保留旧命名接口:仅换调色板(内部仍走统一流程)。
|
||
/// </summary>
|
||
public static void ApplyThemePalette(ThemePalette themePalette) => ApplyTheme(null, themePalette, false);
|
||
|
||
/// <summary>
|
||
/// 保留旧命名接口:仅换明暗模式。
|
||
/// </summary>
|
||
public static void ApplyThemeMode(ThemeMode themeMode) => ApplyTheme(themeMode, null, false);
|
||
|
||
/// <summary>
|
||
/// 快捷切换 Light Dark(保持当前调色板颜色名称,但会加载对应模式版本)。
|
||
/// </summary>
|
||
public static void SwitchThemeMode()
|
||
{
|
||
var target = GetAppThemeMode() == ThemeMode.Light ? ThemeMode.Dark : ThemeMode.Light;
|
||
ApplyTheme(target, _activeThemePalette, false);
|
||
}
|
||
|
||
// === PATCH START: 新增带动画便捷方法 ===
|
||
/// <summary>
|
||
/// 仅切换明暗模式(带动画)。
|
||
/// </summary>
|
||
public static Task SwitchThemeModeAnimatedAsync(int durationMs = 350)
|
||
{
|
||
var target = GetAppThemeMode() == ThemeMode.Light ? ThemeMode.Dark : ThemeMode.Light;
|
||
return ApplyThemeAsync(target, _activeThemePalette, true, durationMs);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同步封装:仅切换明暗模式(带动画)。
|
||
/// </summary>
|
||
public static void SwitchThemeModeAnimated(int durationMs = 350)
|
||
{
|
||
SwitchThemeModeAnimatedAsync(durationMs).GetAwaiter().GetResult();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 指定模式 + 调色板动画切换(语义糖)。
|
||
/// </summary>
|
||
public static Task ApplyThemeAnimatedAsync(ThemeMode mode, ThemePalette palette, int durationMs = 350)
|
||
=> ApplyThemeAsync(mode, palette, true, durationMs);
|
||
// === PATCH END ===
|
||
|
||
/// <summary>
|
||
/// 启用主题持久化(后续每次成功切换会写入本地文件)。
|
||
/// 需在 RestorePersistedTheme 之后调用(避免第一次加载又覆盖未应用的老值)。
|
||
/// </summary>
|
||
public static void EnablePersistence() => _persistenceEnabled = true;
|
||
|
||
/// <summary>
|
||
/// 禁用主题持久化(停止写入文件)。
|
||
/// </summary>
|
||
public static void DisablePersistence() => _persistenceEnabled = false;
|
||
|
||
/// <summary>
|
||
/// 启动时调用:读取上次用户选择并应用(不启用动画,不触发保存)。
|
||
/// 在 App.xaml.cs OnStartup 或 MainWindow 构造最早调用。
|
||
/// </summary>
|
||
public static void RestorePersistedTheme()
|
||
{
|
||
ThemeMode? mode;
|
||
ThemePalette? palette;
|
||
if (ThemePreferenceStore.TryLoad(out mode, out palette) && (mode.HasValue || palette.HasValue))
|
||
{
|
||
var wasEnabled = _persistenceEnabled;
|
||
_persistenceEnabled = false; // 防止恢复时立刻写文件
|
||
try { ApplyTheme(mode, palette, animate: false); }
|
||
finally { _persistenceEnabled = wasEnabled; }
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Debug
|
||
#if DEBUG
|
||
/// <summary>
|
||
/// 调试辅助:输出合并字典当前顺序及其 Source,用于确认是否存在重复主题或顺序错误。
|
||
/// </summary>
|
||
private static void DumpMerged(string tag)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine("==== " + tag + " MergedDictionaries ====");
|
||
int i = 0;
|
||
foreach (var d in Current.Resources.MergedDictionaries)
|
||
System.Diagnostics.Debug.WriteLine($"{i++}: {d.Source}");
|
||
}
|
||
#endif
|
||
#endregion
|
||
|
||
#region UiApplication (原有实例封装)
|
||
private static ThemeManager? _themeManagerInstance;
|
||
private readonly Application? application;
|
||
private Window mainWindow;
|
||
private ResourceDictionary resources;
|
||
|
||
/// <summary>
|
||
/// 私有构造:封装对 Application 的引用;不在此进行主题逻辑(避免启动时副作用)。
|
||
/// </summary>
|
||
private ThemeManager(Application? application)
|
||
{
|
||
if (application == null) return;
|
||
this.application = application;
|
||
System.Diagnostics.Debug.WriteLine($"INFO | {typeof(ThemeManager)} application is {this.application}", ThemeManager.LibraryNamespace);
|
||
}
|
||
|
||
/// <summary>关闭应用(与主题无直接关系,仅做统一封装)</summary>
|
||
public void Shutdown() => application?.Shutdown();
|
||
|
||
/// <summary>尝试获取资源(简单封装)</summary>
|
||
public object TryFindResource(object resourceKey) => Resources[resourceKey];
|
||
|
||
/// <summary>单例访问(懒加载)</summary>
|
||
public static ThemeManager? Current
|
||
{
|
||
get
|
||
{
|
||
if (_themeManagerInstance == null)
|
||
_themeManagerInstance = new ThemeManager(System.Windows.Application.Current);
|
||
return _themeManagerInstance;
|
||
}
|
||
}
|
||
|
||
/// <summary>主窗口引用(不影响主题;可用于窗口内调用 ApplyResourcesToElement)</summary>
|
||
public Window MainWindow
|
||
{
|
||
get => application?.MainWindow ?? mainWindow;
|
||
set
|
||
{
|
||
if (application != null) application.MainWindow = value;
|
||
mainWindow = value;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回应用级 ResourceDictionary:
|
||
/// - 初次访问时构建一个包含 ThemesDictionary(默认 Light) + ControlsDictionary 的集合
|
||
/// - ThemesDictionary 会在首次真正切换时被 ReplaceOrAddDictionary 移除,替换为精确主题字典
|
||
/// - 这样设计允许:设计器/启动阶段有默认资源,后续切换保证唯一
|
||
/// </summary>
|
||
public ResourceDictionary Resources
|
||
{
|
||
get
|
||
{
|
||
if (resources != null) return application?.Resources ?? resources;
|
||
resources = new ResourceDictionary();
|
||
try
|
||
{
|
||
// 注意:ThemesDictionary 内部会加载 Light.xaml(设计期友好)。
|
||
// 后续切换会删除它并插入新的主题字典。
|
||
var themesDictionary = new ThemesDictionary();
|
||
var controlsDictionary = new ControlsDictionary();
|
||
resources.MergedDictionaries.Add(themesDictionary);
|
||
resources.MergedDictionaries.Add(controlsDictionary);
|
||
}
|
||
catch
|
||
{
|
||
// 忽略初始化失败,避免启动崩溃(可根据需要添加日志)
|
||
}
|
||
return application?.Resources ?? resources;
|
||
}
|
||
set
|
||
{
|
||
if (application != null) application.Resources = value;
|
||
resources = value;
|
||
}
|
||
}
|
||
#endregion
|
||
}
|
||
|