using System; using System.Collections.Generic; using System.Windows; using System.Windows.Media; using TeamAAS; namespace TeamAAS.Theme { /// /// 主题画刷覆盖层:主色驱动的专业配色系统核心。 /// /// 原理(对齐 HandyControls fork 的 ThemeResources 机制): /// 把一张【纯值覆盖字典】Add 到应用合并字典【末尾】——WPF 合并字典查找为 /// "后添加的先查"(逆序),覆盖层获得最高查找优先级,元素/样式重查任何 /// 画刷/调色板键都命中这里的纯值,官方大字典被遮蔽。 /// /// 构成: /// 1. 官方基准(全量):启动/整替官方字典后,把调色板 Color 键与画刷以 /// "求值冻结"形式捕获为纯值基准(此时求值链干净,值得官方色彩真传); /// 中性色/语义色沿用官方值,保证与 HC 控件生态协调。 /// 2. 主色派生(专业配色):PrimaryBrush/DarkPrimaryBrush/TitleBrush 及对应 Color 键 /// 由用户主色按 HSL 派生(暗色下主色提亮保对比、hover 变体 darken 12%), /// ReverseTextColor 按主色亮度自动选黑/白(WCAG AA 可读性)。 /// /// 收益: /// - 覆盖画刷是纯值(无 DynamicResource 表达式)→ 天然免疫第三方资源污染; /// - 明暗/主色切换 = 重建轻量覆盖字典(毫秒级)→ 不触碰官方大字典 → 无全窗口样式重算(丝滑); /// - 明暗基准按 dark/light 各缓存一份,首次遇到某明暗需整替官方字典捕获,之后纯覆盖切换。 /// internal static class ThemeOverlay { /// HC 主色系画刷键:值由用户主色派生(而非官方基准)。 private static readonly string[] AccentDrivenBrushes = { "PrimaryBrush", "DarkPrimaryBrush", "TitleBrush" }; /// HC 主色系调色板键:与画刷同步派生。 private static readonly string[] AccentDrivenColors = { "PrimaryColor", "DarkPrimaryColor" }; /// 当前覆盖字典(Purge 放行判据;Add 于应用合并字典末尾)。 private static ResourceDictionary _overlay; /// 官方基准缓存:明暗 → (键 → 官方纯值)。官方值来自整替后的干净字典。 private static readonly Dictionary> _baseCache = new Dictionary>(); /// 当前明暗(整替兜底路径的默认值来源)。 public static bool CurrentDark { get; private set; } /// 当前主色(整替兜底路径重建覆盖时的来源)。 public static Color CurrentAccent { get; private set; } = Color.FromRgb(0x2D, 0x6C, 0xDF); /// 官方基准是否已捕获(某明暗)。 public static bool HasBase(bool dark) { return _baseCache.ContainsKey(dark); } /// /// 捕获官方基准:在官方字典刚被整替(干净、未被污染)时调用。 /// 递归收集白名单官方字典中的 Brush(求值冻结)与 Color 键。 /// public static void CaptureBase(bool dark) { var app = Application.Current; if (app == null) return; try { var baseDict = new Dictionary(StringComparer.Ordinal); CollectOfficial(app.Resources, baseDict); _baseCache[dark] = baseDict; AppLogger.Info( $"[主题覆盖] 已捕获 {(dark ? "暗色" : "浅色")}官方基准({baseDict.Count} 项)", nameof(ThemeOverlay)); } catch (Exception ex) { AppLogger.Error("[主题覆盖] 官方基准捕获失败", ex, nameof(ThemeOverlay)); } } /// 递归收集官方字典中的 Brush(冻结纯值)与 Color 键。 private static void CollectOfficial(ResourceDictionary dict, Dictionary 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 键 } } /// /// 构建并应用覆盖字典(明暗/主色变化的统一入口,毫秒级,丝滑)。 /// 若该明暗的官方基准尚未捕获(首次遇到),返回 false 由调用方先整替官方字典。 /// public static bool Apply(bool dark, Color accent) { Dictionary 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; } /// Purge 放行判据:是否我们的覆盖字典。 public static bool IsOverlay(ResourceDictionary dict) { return dict != null && ReferenceEquals(dict, _overlay); } /// /// 摘除覆盖层(不重建)。整替官方字典前必须调用—— /// 否则基准捕获会被上一轮覆盖值污染(覆盖层查找优先级最高)。 /// public static void RemoveOverlay() { if (_overlay == null) return; try { Application.Current?.Resources.MergedDictionaries.Remove(_overlay); } catch { /* 移除失败忽略 */ } _overlay = null; } /// 官方字典判定(与 VmResourceGuard 同规则:Source 含 HandyControl)。 private static bool IsOfficialDict(ResourceDictionary dict) { try { var src = dict.Source; return src != null && src.OriginalString.Contains("HandyControl"); } catch { return false; } } } }