ThemeManager.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Windows;
  4. using System.Windows.Media;
  5. namespace TeamAAS.Theme
  6. {
  7. /// <summary>
  8. /// 主题门面:主程序唯一的主题调用入口。
  9. ///
  10. /// 职责边界:
  11. /// - 主程序(App/ViewModel)只调用本类,不感知 VmResourceGuard/ThemeOverlay/官方字典细节;
  12. /// - 配置读取(SystemConfig.Theme / AccentColor)由主程序完成后以参数传入,
  13. /// 本模块不反向依赖主程序配置类型(依赖方向:主程序 → Global)。
  14. ///
  15. /// 内部结构(同程序集 internal):
  16. /// - VmResourceGuard:资源守护(切主题入口消毒 / Purge 第三方注入字典 / 污染清理 / 坏画刷整替)
  17. /// - ThemeOverlay:纯值覆盖字典(主色派生配色 + 官方基准遮蔽,免疫第三方污染)
  18. /// - ColorUtility:HSL 颜色派生
  19. /// </summary>
  20. public static class ThemeManager
  21. {
  22. /// <summary>明暗主题变化通知(主界面图标/预览刷新订阅)。</summary>
  23. public static event Action ThemeChanged;
  24. private static bool _dark;
  25. private static Color _accent = Color.FromRgb(0x2D, 0x6C, 0xDF);
  26. /// <summary>按配置应用完整主题(启动 / 设置页保存时调用)。themeSetting:"Dark"/"Light"。</summary>
  27. public static void ApplyTheme(string themeSetting, string accentHex)
  28. {
  29. ApplyTheme(string.Equals(themeSetting, "Dark", StringComparison.OrdinalIgnoreCase), accentHex);
  30. }
  31. /// <summary>按配置应用完整主题(bool 版本)。</summary>
  32. public static void ApplyTheme(bool dark, string accentHex)
  33. {
  34. ApplyTheme(dark, ParseAccent(accentHex));
  35. }
  36. /// <summary>按配置应用完整主题(Color 版本)。</summary>
  37. public static void ApplyTheme(bool dark, Color accent)
  38. {
  39. _dark = dark;
  40. _accent = accent;
  41. UpdateThemeInternal(dark, accent);
  42. }
  43. /// <summary>切换明暗主题(保持当前主色)。</summary>
  44. public static void UpdateTheme(bool dark)
  45. {
  46. _dark = dark;
  47. UpdateThemeInternal(dark, _accent);
  48. }
  49. /// <summary>切换主色(保持当前明暗)。hex 形式如 "#2D6CDF"。</summary>
  50. public static void UpdateAccent(string accentHex)
  51. {
  52. UpdateAccent(ParseAccent(accentHex));
  53. }
  54. /// <summary>切换主色(保持当前明暗)。</summary>
  55. public static void UpdateAccent(Color color)
  56. {
  57. _accent = color;
  58. // 覆盖字典重建(毫秒级,主色系派生 + 文字对比适配);
  59. // 若该明暗的官方基准尚未捕获(异常时序),回退整替路径补齐
  60. if (!ThemeOverlay.Apply(_dark, color))
  61. {
  62. VmResourceGuard.ForceReplaceTheme(_dark);
  63. }
  64. }
  65. /// <summary>安全解析颜色(非法/空值回退默认主色蓝)。</summary>
  66. public static Color ParseAccent(string hex)
  67. {
  68. try
  69. {
  70. if (!string.IsNullOrWhiteSpace(hex))
  71. return (Color)ColorConverter.ConvertFromString(hex);
  72. }
  73. catch { /* 非法颜色值回退默认 */ }
  74. return Color.FromRgb(0x2D, 0x6C, 0xDF);
  75. }
  76. /// <summary>核心切换流程(原 App.UpdateTheme 逻辑)。</summary>
  77. private static void UpdateThemeInternal(bool dark, Color accent)
  78. {
  79. var merged = Application.Current != null
  80. ? Application.Current.Resources.MergedDictionaries
  81. : null;
  82. if (merged == null) return;
  83. // 切主题前先消毒:移除第三方注入字典/字符串污染条目
  84. VmResourceGuard.Sanitize(nameof(UpdateTheme));
  85. // 结构损坏 / 存在坏画刷 / 该明暗的官方基准尚未捕获 → 整替官方字典
  86. // (内部完成:摘覆盖→整替→捕获基准→重建覆盖)
  87. // 否则 → 纯覆盖重建(丝滑,毫秒级)
  88. if (!IsThemeStructureValid(merged) || VmResourceGuard.HasBrokenBrush() || !ThemeOverlay.HasBase(dark))
  89. {
  90. VmResourceGuard.ForceReplaceTheme(dark);
  91. }
  92. else if (!IsTargetSkinAlready(merged, dark))
  93. {
  94. if (!ThemeOverlay.Apply(dark, accent))
  95. {
  96. VmResourceGuard.ForceReplaceTheme(dark);
  97. }
  98. }
  99. ThemeChanged?.Invoke();
  100. }
  101. /// <summary>
  102. /// 判定合并字典是否包含有效的官方主题结构
  103. /// (Skin 调色板字典 + Theme.xaml 画刷/样式字典)。
  104. /// 覆盖层加入后索引会变化,因此按存在性遍历判定。
  105. /// </summary>
  106. private static bool IsThemeStructureValid(IList<ResourceDictionary> merged)
  107. {
  108. bool hasSkin = false;
  109. bool hasTheme = false;
  110. foreach (var d in merged)
  111. {
  112. try
  113. {
  114. var s = d.Source != null ? d.Source.OriginalString : null;
  115. if (s == null) continue;
  116. if (s.Contains("SkinDark.xaml") || s.Contains("SkinDefault.xaml")) hasSkin = true;
  117. else if (s.Contains("Theme.xaml")) hasTheme = true;
  118. }
  119. catch { /* Source 读取异常时跳过 */ }
  120. }
  121. return hasSkin && hasTheme;
  122. }
  123. /// <summary>md 中是否已是目标明暗的调色板字典。</summary>
  124. private static bool IsTargetSkinAlready(IList<ResourceDictionary> merged, bool dark)
  125. {
  126. foreach (var d in merged)
  127. {
  128. try
  129. {
  130. var s = d.Source != null ? d.Source.OriginalString : null;
  131. if (s == null) continue;
  132. if (dark && s.Contains("SkinDark.xaml")) return true;
  133. if (!dark && s.Contains("SkinDefault.xaml")) return true;
  134. }
  135. catch { }
  136. }
  137. return false;
  138. }
  139. }
  140. }