| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- using System;
- using System.Collections.Generic;
- using System.Windows;
- using System.Windows.Media;
- using TeamAAS;
- namespace TeamAAS.Theme
- {
- /// <summary>
- /// 主题画刷覆盖层:主色驱动的专业配色系统核心。
- ///
- /// 原理(对齐 HandyControls fork 的 ThemeResources 机制):
- /// 把一张【纯值覆盖字典】Add 到应用合并字典【末尾】——WPF 合并字典查找为
- /// "后添加的先查"(逆序),覆盖层获得最高查找优先级,元素/样式重查任何
- /// 画刷/调色板键都命中这里的纯值,官方大字典被遮蔽。
- ///
- /// 构成:
- /// 1. 官方基准(全量):启动/整替官方字典后,把调色板 Color 键与画刷以
- /// "求值冻结"形式捕获为纯值基准(此时求值链干净,值得官方色彩真传);
- /// 中性色/语义色沿用官方值,保证与 HC 控件生态协调。
- /// 2. 主色派生(专业配色):PrimaryBrush/DarkPrimaryBrush/TitleBrush 及对应 Color 键
- /// 由用户主色按 HSL 派生(暗色下主色提亮保对比、hover 变体 darken 12%),
- /// ReverseTextColor 按主色亮度自动选黑/白(WCAG AA 可读性)。
- ///
- /// 收益:
- /// - 覆盖画刷是纯值(无 DynamicResource 表达式)→ 天然免疫第三方资源污染;
- /// - 明暗/主色切换 = 重建轻量覆盖字典(毫秒级)→ 不触碰官方大字典 → 无全窗口样式重算(丝滑);
- /// - 明暗基准按 dark/light 各缓存一份,首次遇到某明暗需整替官方字典捕获,之后纯覆盖切换。
- /// </summary>
- internal static class ThemeOverlay
- {
- /// <summary>HC 主色系画刷键:值由用户主色派生(而非官方基准)。</summary>
- private static readonly string[] AccentDrivenBrushes =
- { "PrimaryBrush", "DarkPrimaryBrush", "TitleBrush" };
- /// <summary>HC 主色系调色板键:与画刷同步派生。</summary>
- private static readonly string[] AccentDrivenColors =
- { "PrimaryColor", "DarkPrimaryColor" };
- /// <summary>当前覆盖字典(Purge 放行判据;Add 于应用合并字典末尾)。</summary>
- private static ResourceDictionary _overlay;
- /// <summary>官方基准缓存:明暗 → (键 → 官方纯值)。官方值来自整替后的干净字典。</summary>
- private static readonly Dictionary<bool, Dictionary<string, object>> _baseCache =
- new Dictionary<bool, Dictionary<string, object>>();
- /// <summary>当前明暗(整替兜底路径的默认值来源)。</summary>
- public static bool CurrentDark { get; private set; }
- /// <summary>当前主色(整替兜底路径重建覆盖时的来源)。</summary>
- public static Color CurrentAccent { get; private set; } = Color.FromRgb(0x2D, 0x6C, 0xDF);
- /// <summary>官方基准是否已捕获(某明暗)。</summary>
- public static bool HasBase(bool dark)
- {
- return _baseCache.ContainsKey(dark);
- }
- /// <summary>
- /// 捕获官方基准:在官方字典刚被整替(干净、未被污染)时调用。
- /// 递归收集白名单官方字典中的 Brush(求值冻结)与 Color 键。
- /// </summary>
- public static void CaptureBase(bool dark)
- {
- var app = Application.Current;
- if (app == null) return;
- try
- {
- var baseDict = new Dictionary<string, object>(StringComparer.Ordinal);
- CollectOfficial(app.Resources, baseDict);
- _baseCache[dark] = baseDict;
- AppLogger.Info(
- $"[主题覆盖] 已捕获 {(dark ? "暗色" : "浅色")}官方基准({baseDict.Count} 项)",
- nameof(ThemeOverlay));
- }
- catch (Exception ex)
- {
- AppLogger.Error("[主题覆盖] 官方基准捕获失败", ex, nameof(ThemeOverlay));
- }
- }
- /// <summary>递归收集官方字典中的 Brush(冻结纯值)与 Color 键。</summary>
- private static void CollectOfficial(ResourceDictionary dict, Dictionary<string, object> into)
- {
- // 只认官方字典(第三方注入字典由 Purge 负责移除,不进基准)
- var merged = dict.MergedDictionaries;
- for (int i = 0; i < merged.Count; i++)
- {
- var child = merged[i];
- if (IsOfficialDict(child)) CollectOfficial(child, into);
- }
- foreach (var key in dict.Keys)
- {
- var keyName = key as string;
- if (keyName == null) continue;
- object value;
- try { value = dict[key]; }
- catch { continue; }
- var brush = value as Brush;
- if (brush != null)
- {
- try { into[keyName] = brush.GetCurrentValueAsFrozen(); } // 求值冻结:官方当前色纯值
- catch { /* 求值失败(如被污染)跳过,该键保持未覆盖 */ }
- continue;
- }
- if (value is Color) into[keyName] = value; // 调色板 Color 键
- }
- }
- /// <summary>
- /// 构建并应用覆盖字典(明暗/主色变化的统一入口,毫秒级,丝滑)。
- /// 若该明暗的官方基准尚未捕获(首次遇到),返回 false 由调用方先整替官方字典。
- /// </summary>
- public static bool Apply(bool dark, Color accent)
- {
- Dictionary<string, object> baseDict;
- if (!_baseCache.TryGetValue(dark, out baseDict)) return false;
- var app = Application.Current;
- if (app == null) return true;
- try
- {
- // 记录当前主题状态(整替兜底路径的恢复来源)
- CurrentDark = dark;
- CurrentAccent = accent;
- // 主色在暗色下轻微提亮,保证在深背景上的对比度(专业配色的明暗适配)
- var effectiveAccent = dark ? ColorUtility.Lighten(accent, 0.08) : accent;
- var hoverAccent = ColorUtility.Darken(effectiveAccent, 0.12);
- var textOnAccent = ColorUtility.ContrastText(effectiveAccent);
- var overlay = new ResourceDictionary();
- // 1) 官方基准全量(纯值:中性色/语义色沿用官方,天然协调)
- foreach (var kv in baseDict)
- {
- overlay[kv.Key] = kv.Value;
- }
- // 2) 主色系派生(覆盖基准中的官方主色)
- foreach (var key in AccentDrivenBrushes)
- {
- if (!baseDict.ContainsKey(key)) continue;
- Color c;
- if (key == "DarkPrimaryBrush") c = hoverAccent;
- else c = effectiveAccent;
- var brush = new SolidColorBrush(c);
- brush.Freeze();
- overlay[key] = brush;
- }
- foreach (var key in AccentDrivenColors)
- {
- if (!baseDict.ContainsKey(key)) continue;
- overlay[key] = key == "DarkPrimaryColor" ? hoverAccent : effectiveAccent;
- }
- // 3) 主色上的文字对比色:写入 ReverseTextColor(HC 官方语义即"反色文字",用于
- // 主色背景上的按钮文字等),按主色亮度自动选黑/白(避免浅色主色+白字看不清)。
- // 【注意】绝不能动 PrimaryTextBrush——那是全局正文主文本色,明暗适配由官方
- // 调色板负责;若按主色派生会让浅色主题正文全变白字(白底白字,实测 2026-09-04)
- if (baseDict.ContainsKey("ReverseTextColor"))
- {
- var brush = new SolidColorBrush(textOnAccent);
- brush.Freeze();
- overlay["ReverseTextColor"] = brush;
- }
- // 4) 替换/插入覆盖层。
- // 【关键】WPF 合并字典的查找顺序是"后添加的先查"(逆序):
- // 覆盖层必须 Add 到合并字典【末尾】才能获得最高查找优先级;
- // 若 Insert(0) 放最前面,反而会被 Theme.xaml 的旧画刷遮蔽(实测:切换后颜色不动)
- var merged = app.Resources.MergedDictionaries;
- if (_overlay != null) merged.Remove(_overlay);
- _overlay = overlay;
- merged.Add(_overlay);
- }
- catch (Exception ex)
- {
- AppLogger.Error("[主题覆盖] 覆盖字典构建失败", ex, nameof(ThemeOverlay));
- }
- return true;
- }
- /// <summary>Purge 放行判据:是否我们的覆盖字典。</summary>
- public static bool IsOverlay(ResourceDictionary dict)
- {
- return dict != null && ReferenceEquals(dict, _overlay);
- }
- /// <summary>
- /// 摘除覆盖层(不重建)。整替官方字典前必须调用——
- /// 否则基准捕获会被上一轮覆盖值污染(覆盖层查找优先级最高)。
- /// </summary>
- public static void RemoveOverlay()
- {
- if (_overlay == null) return;
- try { Application.Current?.Resources.MergedDictionaries.Remove(_overlay); }
- catch { /* 移除失败忽略 */ }
- _overlay = null;
- }
- /// <summary>官方字典判定(与 VmResourceGuard 同规则:Source 含 HandyControl)。</summary>
- private static bool IsOfficialDict(ResourceDictionary dict)
- {
- try
- {
- var src = dict.Source;
- return src != null && src.OriginalString.Contains("HandyControl");
- }
- catch
- {
- return false;
- }
- }
- }
- }
|