mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-15 17:30:27 +08:00
feat: 莫奈取色 (#17242)
* refactor: 移动关卡配置字段到 FightTask UseAlternateStage / HideUnavailableStage / CustomStageCode 已迁移至 FightTask.cs,从 GUI 全局配置中移除冗余定义 * feat: 莫奈取色 — 配置与本地化 新增 BackgroundMonetEnabled / BackgroundMonetMode / BackgroundMonetCustomColor 配置字段,以及 MonetModeType 枚举(Auto / Custom)。 添加 zh-cn / zh-tw / en-us / ja-jp / ko-kr 五语本地化字符串 * feat: 莫奈取色 — 调色板生成与应用逻辑 - MonetPaletteHelper: 基于 HSL 线性计算生成主题色板,主色 / 文字 / 背景角色各自有独立的饱和度与明度策略;明度随背景图不透明度连续 自适应,保证前景与有效背景之间始终有足够对比度 - ColorExtractorHelper: 从背景图 BitmapSource 提取主色 - ThemeHelper: ApplyMonetPalette / RevertMonetPalette / ReapplyMonet, 计算放后台线程、资源写入在 UI 线程;修正 AccentColor 覆盖顺序、 主题切换时清除遮蔽资源以恢复正确主题色 * feat: 莫奈取色 — 设置界面与 ViewModel - BackgroundSettings.xaml: 新增莫奈取色开关、取色模式选择、自定义颜色 色块预览与 ColorPicker 按钮 - BackgroundSettingsUserControlModel: UpdateMonet / ScheduleMonetUpdate 防抖逻辑,不透明度滑块拖动时后台计算 + UI 线程写入 - SettingsViewModel: 主题初始化完成后恢复莫奈调色板 * chore: review * chore: 调整间距
This commit is contained in:
@@ -43,12 +43,6 @@ public class GUI : INotifyPropertyChanged
|
||||
|
||||
public bool SaveWindowPlacement { get; set; } = true;
|
||||
|
||||
public bool UseAlternateStage { get; set; } = false;
|
||||
|
||||
public bool HideUnavailableStage { get; set; } = true;
|
||||
|
||||
public bool CustomStageCode { get; set; } = false;
|
||||
|
||||
public InverseClearType InverseClearMode { get; set; } = InverseClearType.Clear;
|
||||
|
||||
public string WindowTitlePrefix { get; set; } = string.Empty;
|
||||
@@ -85,6 +79,13 @@ public class GUI : INotifyPropertyChanged
|
||||
|
||||
public bool ExpanderAboutUs { get; set; } = true;
|
||||
|
||||
// ===== 背景设置(莫奈取色) =====
|
||||
public bool BackgroundMonetEnabled { get; set; } = false;
|
||||
|
||||
public MonetModeType BackgroundMonetMode { get; set; } = MonetModeType.Auto;
|
||||
|
||||
public string BackgroundMonetCustomColor { get; set; } = "#326CF3";
|
||||
|
||||
[UsedImplicitly]
|
||||
public void OnPropertyChanged(string propertyName, object before, object after)
|
||||
{
|
||||
@@ -129,4 +130,20 @@ public class GUI : INotifyPropertyChanged
|
||||
/// </summary>
|
||||
ClearInverse,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 莫奈取色的模式。
|
||||
/// </summary>
|
||||
public enum MonetModeType
|
||||
{
|
||||
/// <summary>
|
||||
/// 从背景图自动提取主色。
|
||||
/// </summary>
|
||||
Auto = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 用户手动选择颜色。
|
||||
/// </summary>
|
||||
Custom,
|
||||
}
|
||||
}
|
||||
|
||||
192
src/MaaWpfGui/Helper/ColorExtractorHelper.cs
Normal file
192
src/MaaWpfGui/Helper/ColorExtractorHelper.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
// <copyright file="ColorExtractorHelper.cs" company="MaaAssistantArknights">
|
||||
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
|
||||
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License v3.0 only as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace MaaWpfGui.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// 从图片中提取主色(dominant color)的工具类。
|
||||
/// 采用降采样 + 颜色量化桶统计的方式,性能优于完整的 k-means。
|
||||
/// </summary>
|
||||
public static class ColorExtractorHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 量化桶步长,将 RGB 空间按每 <see cref="BucketSize"/> 为一格分桶。
|
||||
/// </summary>
|
||||
private const int BucketSize = 16;
|
||||
|
||||
/// <summary>
|
||||
/// 降采样后的最大边长(像素),用于加速取色。
|
||||
/// </summary>
|
||||
private const int SampleSize = 32;
|
||||
|
||||
/// <summary>
|
||||
/// 从 BitmapImage 中提取出现频率最高的颜色。
|
||||
/// </summary>
|
||||
/// <param name="image">源图片。必须已调用过EndInit并完成解码。</param>
|
||||
/// <returns>提取到的主色;若图片无效则返回白色。</returns>
|
||||
public static Color ExtractDominantColor(BitmapSource image)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return Colors.White;
|
||||
}
|
||||
|
||||
// 降采样到 ~32×32 以加速
|
||||
var scaledWidth = Math.Min(SampleSize, image.PixelWidth);
|
||||
var scaledHeight = Math.Min(SampleSize, image.PixelHeight);
|
||||
|
||||
var converted = new FormatConvertedBitmap(image, PixelFormats.Bgra32, null, 0);
|
||||
var resized = new TransformedBitmap(converted, new ScaleTransform(
|
||||
(double)scaledWidth / image.PixelWidth,
|
||||
(double)scaledHeight / image.PixelHeight));
|
||||
|
||||
var pixels = new byte[scaledWidth * scaledHeight * 4];
|
||||
resized.CopyPixels(pixels, scaledWidth * 4, 0);
|
||||
|
||||
// 量化到桶,统计频率
|
||||
var buckets = new Dictionary<long, (long Count, long R, long G, long B)>();
|
||||
|
||||
for (var i = 0; i < pixels.Length; i += 4)
|
||||
{
|
||||
var a = pixels[i + 3];
|
||||
if (a < 128)
|
||||
{
|
||||
// 跳过近透明像素
|
||||
continue;
|
||||
}
|
||||
|
||||
var r = pixels[i + 2]; // BGRA
|
||||
var g = pixels[i + 1];
|
||||
var b = pixels[i];
|
||||
|
||||
var bucketR = r / BucketSize;
|
||||
var bucketG = g / BucketSize;
|
||||
var bucketB = b / BucketSize;
|
||||
var key = (((bucketR * 256L) + bucketG) * 256L) + bucketB;
|
||||
|
||||
if (buckets.TryGetValue(key, out var entry))
|
||||
{
|
||||
buckets[key] = (entry.Count + 1, entry.R + r, entry.G + g, entry.B + b);
|
||||
}
|
||||
else
|
||||
{
|
||||
buckets[key] = (1, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
if (buckets.Count == 0)
|
||||
{
|
||||
return Colors.White;
|
||||
}
|
||||
|
||||
// 对每个桶计算「频率 × 鲜艳度」的综合得分,优先选高占比且鲜艳的颜色
|
||||
Color bestColor = Colors.White;
|
||||
var bestScore = -1.0;
|
||||
|
||||
foreach (var (_, value) in buckets)
|
||||
{
|
||||
long count = value.Count;
|
||||
var avgR = (byte)(value.R / count);
|
||||
var avgG = (byte)(value.G / count);
|
||||
var avgB = (byte)(value.B / count);
|
||||
var avgColor = Color.FromRgb(avgR, avgG, avgB);
|
||||
|
||||
var (_, sat, _) = RgbToHsl(avgColor);
|
||||
|
||||
// 频率占比 × 饱和度²,饱和度平方确保鲜艳颜色优先
|
||||
var frequencyRatio = (double)count / (scaledWidth * scaledHeight);
|
||||
var score = frequencyRatio * sat * sat;
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestColor = avgColor;
|
||||
}
|
||||
}
|
||||
|
||||
return bestColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步版本的取色方法,将计算放到后台线程。
|
||||
/// </summary>
|
||||
/// <param name="image">源图片。</param>
|
||||
/// <returns>提取到的主色。</returns>
|
||||
public static Task<Color> ExtractDominantColorAsync(BitmapSource image)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return Task.FromResult(Colors.White);
|
||||
}
|
||||
|
||||
// BitmapImage 必须 Freeze 后才能跨线程访问像素
|
||||
if (!image.IsFrozen)
|
||||
{
|
||||
image.Freeze();
|
||||
}
|
||||
|
||||
return Task.Run(() => ExtractDominantColor(image));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RGB → HSL 转换。
|
||||
/// </summary>
|
||||
private static (double H, double S, double L) RgbToHsl(Color color)
|
||||
{
|
||||
var r = color.R / 255.0;
|
||||
var g = color.G / 255.0;
|
||||
var b = color.B / 255.0;
|
||||
|
||||
var max = Math.Max(r, Math.Max(g, b));
|
||||
var min = Math.Min(r, Math.Min(g, b));
|
||||
var delta = max - min;
|
||||
|
||||
var l = (max + min) / 2.0;
|
||||
|
||||
if (delta == 0)
|
||||
{
|
||||
return (0, 0, l);
|
||||
}
|
||||
|
||||
var s = delta / (1 - Math.Abs((2 * l) - 1));
|
||||
|
||||
double h;
|
||||
if (max == r)
|
||||
{
|
||||
h = ((g - b) / delta) % 6;
|
||||
}
|
||||
else if (max == g)
|
||||
{
|
||||
h = ((b - r) / delta) + 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
h = ((r - g) / delta) + 4;
|
||||
}
|
||||
|
||||
h *= 60;
|
||||
if (h < 0)
|
||||
{
|
||||
h += 360;
|
||||
}
|
||||
|
||||
return (h, s, l);
|
||||
}
|
||||
}
|
||||
266
src/MaaWpfGui/Helper/MonetPaletteHelper.cs
Normal file
266
src/MaaWpfGui/Helper/MonetPaletteHelper.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
// <copyright file="MonetPaletteHelper.cs" company="MaaAssistantArknights">
|
||||
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
|
||||
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License v3.0 only as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace MaaWpfGui.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// 根据提取到的主色,通过 HSL 线性计算生成 Material You 风格的主题色板。
|
||||
/// <para>所有颜色均由 (色相, 饱和度, 明度) 直接计算,无离散档位量化,保证连续平滑。</para>
|
||||
/// <para>文字与背景之间保持足够的明度距离以保证可读性。</para>
|
||||
/// </summary>
|
||||
public static class MonetPaletteHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 需要生成的主题资源 key 列表。
|
||||
/// </summary>
|
||||
public static readonly string[] PaletteKeys =
|
||||
[
|
||||
"PrimaryBrush",
|
||||
"DarkPrimaryBrush",
|
||||
"LightPrimaryBrush",
|
||||
"TitleBrush",
|
||||
"RegionBrush",
|
||||
"RegionBrushOpacity10",
|
||||
"RegionBrushOpacity25",
|
||||
"RegionBrushOpacity50",
|
||||
"RegionBrushOpacity75",
|
||||
"MouseOverRegionBrush",
|
||||
"MouseOverRegionBrushOpacity10",
|
||||
"MouseOverRegionBrushOpacity25",
|
||||
"MouseOverRegionBrushOpacity50",
|
||||
"MouseOverRegionBrushOpacity75",
|
||||
|
||||
// 亮度依赖的文字色:随明暗模式翻转,需跟随 region 明度适配
|
||||
"PrimaryTextBrush",
|
||||
"ThirdlyTextBrush",
|
||||
"TraceLogBrush",
|
||||
"MessageLogBrush",
|
||||
];
|
||||
|
||||
// 原 Dark.xaml / Light.xaml 的 Alpha 值,保持不变
|
||||
private const byte Alpha10 = 0x19; // 10%
|
||||
private const byte Alpha25 = 0x40; // 25%
|
||||
private const byte Alpha50 = 0x7F; // 50%
|
||||
private const byte Alpha75 = 0xBF; // 75%
|
||||
|
||||
// ── 各角色的饱和度常量 ──
|
||||
|
||||
/// <summary>
|
||||
/// 主色(Primary)饱和度下限。原始主色饱和度不足此值时使用此值。
|
||||
/// </summary>
|
||||
private const double PrimaryMinSaturation = 0.55;
|
||||
|
||||
/// <summary>
|
||||
/// 背景体系(Neutral Tint)饱和度。
|
||||
/// </summary>
|
||||
private const double BackgroundSaturation = 0.16;
|
||||
|
||||
/// <summary>
|
||||
/// 文字色饱和度,极低饱和度接近中性色。
|
||||
/// </summary>
|
||||
private const double TextSaturation = 0.06;
|
||||
|
||||
// ── 对比度常量 ──
|
||||
|
||||
/// <summary>
|
||||
/// 文字与有效背景之间要求的最小明度差。
|
||||
/// 原版深色主题 PrimaryText(L=0.90) 与 Region(L=0.11) 差约 0.79。
|
||||
/// </summary>
|
||||
private const double TextContrastDistance = 0.70;
|
||||
|
||||
/// <summary>
|
||||
/// 主色(PrimaryBrush)与有效背景之间要求的最小明度差。
|
||||
/// 原版 Primary(L=0.57) 与 Region(L=0.11) 差约 0.46。
|
||||
/// </summary>
|
||||
private const double PrimaryContrastDistance = 0.45;
|
||||
|
||||
// ── 固定明度常量(不随透明度变化的角色) ──
|
||||
private const double RegionDarkL = 0.10;
|
||||
private const double RegionLightL = 0.90;
|
||||
private const double MouseOverDarkL = 0.20;
|
||||
private const double MouseOverLightL = 0.70;
|
||||
|
||||
/// <summary>
|
||||
/// RegionBrushOpacity25 中底层(混合底)的占比(75%)。
|
||||
/// Opacity25 覆盖层为 25% RegionBrush + 75% 混合底,
|
||||
/// 此常量即混合底在 effectiveRegionL 公式中的权重。
|
||||
/// </summary>
|
||||
private const double Alpha25Factor = 0.75;
|
||||
|
||||
/// <summary>
|
||||
/// 背景图对有效背景明度的影响因子。
|
||||
/// 背景图虽然透过半透明层影响视觉,但文字的可读性主要取决于 RegionBrush 系列底色,
|
||||
/// 因此只取背景图影响的 50%,避免文字色被背景图过度拉偏。
|
||||
/// </summary>
|
||||
private const double BackgroundInfluence = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// 根据基础主色生成调色板。
|
||||
/// </summary>
|
||||
/// <param name="baseColor">提取或用户选定的主色。</param>
|
||||
/// <param name="isDark">当前是否为深色模式。</param>
|
||||
/// <param name="backgroundOpacity">背景图不透明度 (0~100),影响文字色的明度选取。</param>
|
||||
/// <returns>资源 key → 颜色的映射。</returns>
|
||||
public static Dictionary<string, Color> Generate(Color baseColor, bool isDark, int backgroundOpacity = 50)
|
||||
{
|
||||
var (hue, sat, _) = RgbToHsl(baseColor);
|
||||
|
||||
// 主色饱和度:保留原色鲜艳度,但设下限避免过低饱和
|
||||
var primarySat = Math.Max(sat, PrimaryMinSaturation);
|
||||
|
||||
// 有效背景明度:文字实际位于 RegionBrushOpacity25 覆盖层之上。
|
||||
// 背景图透过半透明层影响视觉,但对可读性的影响有限,只取部分影响。
|
||||
var regionL = isDark ? RegionDarkL : RegionLightL;
|
||||
var baseL = RgbToHsl(baseColor).L;
|
||||
var alpha = backgroundOpacity / 100.0;
|
||||
var blendedL = (regionL * (1 - (alpha * BackgroundInfluence))) + (baseL * (alpha * BackgroundInfluence));
|
||||
|
||||
// Opacity25 覆盖层:25% region + 75% 混合底
|
||||
var effectiveRegionL = (blendedL * Alpha25Factor) + (regionL * (1 - Alpha25Factor));
|
||||
|
||||
// ── 固定明度角色:背景体系 ──
|
||||
var region = HslToRgb(hue, BackgroundSaturation, isDark ? RegionDarkL : RegionLightL);
|
||||
var mouseOver = HslToRgb(hue, BackgroundSaturation, isDark ? MouseOverDarkL : MouseOverLightL);
|
||||
|
||||
// ── 自适应明度角色:主色系(随有效背景明度连续变化) ──
|
||||
var primaryTargetL = isDark
|
||||
? Math.Min(effectiveRegionL + PrimaryContrastDistance, 0.90)
|
||||
: Math.Max(effectiveRegionL - PrimaryContrastDistance, 0.10);
|
||||
var primary = HslToRgb(hue, primarySat, primaryTargetL);
|
||||
|
||||
var darkPrimary = isDark
|
||||
? HslToRgb(hue, primarySat, Math.Max(primaryTargetL - 0.10, 0.05))
|
||||
: primary;
|
||||
var lightPrimary = HslToRgb(hue, primarySat, isDark ? 0.10 : 0.70);
|
||||
|
||||
// ── 自适应明度角色:文字系 ──
|
||||
var textTargetL = isDark
|
||||
? Math.Min(effectiveRegionL + TextContrastDistance, 0.90)
|
||||
: Math.Max(effectiveRegionL - TextContrastDistance, 0.05);
|
||||
var traceTargetL = isDark
|
||||
? Math.Min(effectiveRegionL + (TextContrastDistance * 0.7), 0.90)
|
||||
: Math.Max(effectiveRegionL - (TextContrastDistance * 0.7), 0.05);
|
||||
|
||||
var primaryText = HslToRgb(hue, TextSaturation, textTargetL);
|
||||
var traceLog = HslToRgb(hue, TextSaturation, traceTargetL);
|
||||
|
||||
var palette = new Dictionary<string, Color>
|
||||
{
|
||||
["PrimaryBrush"] = primary,
|
||||
["DarkPrimaryBrush"] = darkPrimary,
|
||||
["LightPrimaryBrush"] = lightPrimary,
|
||||
["TitleBrush"] = primary,
|
||||
["RegionBrush"] = region,
|
||||
["MouseOverRegionBrush"] = mouseOver,
|
||||
["PrimaryTextBrush"] = primaryText,
|
||||
["ThirdlyTextBrush"] = Color.FromArgb(0x7F, primaryText.R, primaryText.G, primaryText.B),
|
||||
["TraceLogBrush"] = traceLog,
|
||||
["MessageLogBrush"] = primaryText,
|
||||
};
|
||||
|
||||
// Opacity 系列
|
||||
palette["RegionBrushOpacity10"] = Color.FromArgb(Alpha10, region.R, region.G, region.B);
|
||||
palette["RegionBrushOpacity25"] = Color.FromArgb(Alpha25, region.R, region.G, region.B);
|
||||
palette["RegionBrushOpacity50"] = Color.FromArgb(Alpha50, region.R, region.G, region.B);
|
||||
palette["RegionBrushOpacity75"] = Color.FromArgb(Alpha75, region.R, region.G, region.B);
|
||||
|
||||
palette["MouseOverRegionBrushOpacity10"] = Color.FromArgb(Alpha10, mouseOver.R, mouseOver.G, mouseOver.B);
|
||||
palette["MouseOverRegionBrushOpacity25"] = Color.FromArgb(Alpha25, mouseOver.R, mouseOver.G, mouseOver.B);
|
||||
palette["MouseOverRegionBrushOpacity50"] = Color.FromArgb(Alpha50, mouseOver.R, mouseOver.G, mouseOver.B);
|
||||
palette["MouseOverRegionBrushOpacity75"] = Color.FromArgb(Alpha75, mouseOver.R, mouseOver.G, mouseOver.B);
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RGB → HSL 转换。
|
||||
/// </summary>
|
||||
private static (double H, double S, double L) RgbToHsl(Color color)
|
||||
{
|
||||
var r = color.R / 255.0;
|
||||
var g = color.G / 255.0;
|
||||
var b = color.B / 255.0;
|
||||
|
||||
var max = Math.Max(r, Math.Max(g, b));
|
||||
var min = Math.Min(r, Math.Min(g, b));
|
||||
var delta = max - min;
|
||||
|
||||
var l = (max + min) / 2.0;
|
||||
|
||||
if (delta == 0)
|
||||
{
|
||||
return (0, 0, l);
|
||||
}
|
||||
|
||||
var s = delta / (1 - Math.Abs((2 * l) - 1));
|
||||
|
||||
double h;
|
||||
if (max == r)
|
||||
{
|
||||
h = ((g - b) / delta) % 6;
|
||||
}
|
||||
else if (max == g)
|
||||
{
|
||||
h = ((b - r) / delta) + 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
h = ((r - g) / delta) + 4;
|
||||
}
|
||||
|
||||
h *= 60;
|
||||
if (h < 0)
|
||||
{
|
||||
h += 360;
|
||||
}
|
||||
|
||||
return (h, s, l);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HSL → RGB 转换。
|
||||
/// </summary>
|
||||
private static Color HslToRgb(double h, double s, double l)
|
||||
{
|
||||
if (s == 0)
|
||||
{
|
||||
var gray = (byte)(l * 255);
|
||||
return Color.FromRgb(gray, gray, gray);
|
||||
}
|
||||
|
||||
var c = (1 - Math.Abs((2 * l) - 1)) * s;
|
||||
var hp = h / 60.0;
|
||||
var x = c * (1 - Math.Abs((hp % 2) - 1));
|
||||
|
||||
double r1, g1, b1;
|
||||
switch ((int)hp)
|
||||
{
|
||||
case 0: r1 = c; g1 = x; b1 = 0; break;
|
||||
case 1: r1 = x; g1 = c; b1 = 0; break;
|
||||
case 2: r1 = 0; g1 = c; b1 = x; break;
|
||||
case 3: r1 = 0; g1 = x; b1 = c; break;
|
||||
case 4: r1 = x; g1 = 0; b1 = c; break;
|
||||
default: r1 = c; g1 = 0; b1 = x; break;
|
||||
}
|
||||
|
||||
var m = l - (c / 2.0);
|
||||
return Color.FromRgb(
|
||||
(byte)((r1 + m) * 255),
|
||||
(byte)((g1 + m) * 255),
|
||||
(byte)((b1 + m) * 255));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@
|
||||
// but WITHOUT ANY WARRANTY
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using HandyControl.Themes;
|
||||
@@ -36,6 +39,9 @@ public static class ThemeHelper
|
||||
}
|
||||
|
||||
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
|
||||
|
||||
// 系统主题切换后,莫奈调色板需要按新的明暗模式重新生成
|
||||
ReapplyMonet();
|
||||
}
|
||||
|
||||
public static void SwitchToLightMode()
|
||||
@@ -44,6 +50,7 @@ public static class ThemeHelper
|
||||
ThemeManager.Current.UsingWindowsAppTheme = false;
|
||||
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light;
|
||||
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
|
||||
ReapplyMonet();
|
||||
}
|
||||
|
||||
public static void SwitchToDarkMode()
|
||||
@@ -52,6 +59,7 @@ public static class ThemeHelper
|
||||
ThemeManager.Current.UsingWindowsAppTheme = false;
|
||||
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark;
|
||||
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
|
||||
ReapplyMonet();
|
||||
}
|
||||
|
||||
public static void SwitchToSyncWithOsMode()
|
||||
@@ -67,10 +75,188 @@ public static class ThemeHelper
|
||||
}
|
||||
|
||||
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
|
||||
ReapplyMonet();
|
||||
}
|
||||
|
||||
#endregion Swith Theme
|
||||
|
||||
#region Monet(莫奈取色)
|
||||
|
||||
/// <summary>
|
||||
/// 缓存的莫奈调色板的生成参数,用于明暗切换后重新生成。
|
||||
/// 为 null 表示莫奈取色未启用。
|
||||
/// </summary>
|
||||
private static Color? _monetBaseColor;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存的背景图不透明度,用于 ReapplyMonet 时重新生成文字色。
|
||||
/// </summary>
|
||||
private static int _monetBackgroundOpacity = 50;
|
||||
|
||||
/// <summary>
|
||||
/// 保存被莫奈覆盖前的原始 brush,用于关闭莫奈时恢复。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, SolidColorBrush> _monetOriginalBrushes = [];
|
||||
|
||||
/// <summary>
|
||||
/// 莫奈取色是否已启用(即是否已调用过 ApplyMonetPalette 且未 Revert)。
|
||||
/// </summary>
|
||||
public static bool IsMonetActive => _monetBaseColor != null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否处于深色模式。
|
||||
/// </summary>
|
||||
/// <returns>深色模式返回 true。</returns>
|
||||
public static bool IsDarkMode()
|
||||
{
|
||||
return ThemeManager.Current.ApplicationTheme == ApplicationTheme.Dark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用莫奈调色板,覆盖主题资源中的主色系和背景色系。
|
||||
/// 首次调用时会保存原始值以供回退。
|
||||
/// </summary>
|
||||
/// <param name="baseColor">提取或用户选定的主色。</param>
|
||||
/// <param name="backgroundOpacity">背景图不透明度 (0~100),影响文字色的明度选取。</param>
|
||||
public static void ApplyMonetPalette(Color baseColor, int backgroundOpacity = 50)
|
||||
{
|
||||
var isDark = IsDarkMode();
|
||||
_monetBaseColor = baseColor;
|
||||
_monetBackgroundOpacity = backgroundOpacity;
|
||||
|
||||
// 调色板计算可在任意线程完成,无需占用 UI 线程
|
||||
var palette = MonetPaletteHelper.Generate(baseColor, isDark, backgroundOpacity);
|
||||
|
||||
// 只有实际写入 WPF 资源才需要在 UI 线程执行
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ApplyPaletteToResources(palette, baseColor);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步应用莫奈调色板:计算在后台线程,资源写入在 UI 线程。
|
||||
/// 适用于滑块拖动等高频调用场景,避免阻塞 UI。
|
||||
/// </summary>
|
||||
/// <param name="baseColor">提取或用户选定的主色。</param>
|
||||
/// <param name="backgroundOpacity">背景图不透明度 (0~100),影响文字色的明度选取。</param>
|
||||
/// <param name="cancellationToken">取消令牌,用于防抖时取消过期的调色板计算。</param>
|
||||
/// <returns>表示异步操作的任务。</returns>
|
||||
public static async Task ApplyMonetPaletteAsync(Color baseColor, int backgroundOpacity = 50, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var isDark = IsDarkMode();
|
||||
_monetBaseColor = baseColor;
|
||||
_monetBackgroundOpacity = backgroundOpacity;
|
||||
|
||||
// 在后台线程计算调色板
|
||||
var palette = await Task.Run(() => MonetPaletteHelper.Generate(baseColor, isDark, backgroundOpacity), cancellationToken)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
// 在 UI 线程写入资源
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ApplyPaletteToResources(palette, baseColor);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将已计算好的调色板写入 WPF 资源(必须在 UI 线程调用)。
|
||||
/// </summary>
|
||||
private static void ApplyPaletteToResources(Dictionary<string, Color> palette, Color baseColor)
|
||||
{
|
||||
// 首次应用时保存原始值
|
||||
SaveOriginalsIfNeeded(MonetPaletteHelper.PaletteKeys);
|
||||
|
||||
// 先设置 AccentColor:HandyControl 的 ThemeManager 会根据它重新生成
|
||||
// PrimaryBrush / DarkPrimaryBrush / TitleBrush 等内部资源,
|
||||
// 因此必须在 AccentColor 之后再覆盖为莫奈调色板的值,否则会被覆盖回去
|
||||
ThemeManager.Current.AccentColor = new SolidColorBrush(baseColor);
|
||||
|
||||
foreach (var (key, color) in palette)
|
||||
{
|
||||
Application.Current.Resources[key] = new SolidColorBrush(color);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回退莫奈调色板,恢复所有被覆盖的 brush 到原始值。
|
||||
/// </summary>
|
||||
public static void RevertMonetPalette()
|
||||
{
|
||||
if (_monetBaseColor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_monetBaseColor = null;
|
||||
|
||||
// 只有资源写入需要在 UI 线程
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
// 先恢复 AccentColor 到系统值(同样会重写 HandyControl 内部 brush)
|
||||
if (!WineRuntimeInformation.IsRunningUnderWine)
|
||||
{
|
||||
ThemeManager.Current.AccentColor = ThemeManager.Current.GetAccentColorFromSystem();
|
||||
}
|
||||
|
||||
// 在 AccentColor 之后再恢复原始 brush,避免被 HandyControl 覆盖
|
||||
foreach (var (key, brush) in _monetOriginalBrushes)
|
||||
{
|
||||
Application.Current.Resources[key] = brush;
|
||||
}
|
||||
|
||||
_monetOriginalBrushes.Clear();
|
||||
|
||||
Application.Current.Resources["TitleBrush"] = ThemeManager.Current.AccentColor;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当莫奈取色启用时,使用缓存的 baseColor 重新生成调色板。
|
||||
/// 用于明暗模式切换后同步更新。
|
||||
/// </summary>
|
||||
public static void ReapplyMonet()
|
||||
{
|
||||
if (_monetBaseColor is Color color)
|
||||
{
|
||||
// 明暗模式切换后,HandyControl 会通过 ThemeResources 切换底层主题字典,
|
||||
// 但莫奈之前直接写入 Application.Current.Resources 的值会遮蔽新主题的值。
|
||||
// 需要先移除这些直接覆盖项,让底层新主题值透出,再重新保存和生成。
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var key in MonetPaletteHelper.PaletteKeys)
|
||||
{
|
||||
Application.Current.Resources.Remove(key);
|
||||
}
|
||||
});
|
||||
|
||||
_monetOriginalBrushes.Clear();
|
||||
ApplyMonetPalette(color, _monetBackgroundOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 首次覆盖前保存原始 brush 值。
|
||||
/// </summary>
|
||||
private static void SaveOriginalsIfNeeded(IEnumerable<string> keys)
|
||||
{
|
||||
if (_monetOriginalBrushes.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (Application.Current?.Resources[key] is SolidColorBrush brush)
|
||||
{
|
||||
// 冻结副本,避免引用被修改
|
||||
_monetOriginalBrushes[key] = brush.Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Monet(莫奈取色)
|
||||
|
||||
#region Check UiLogColor
|
||||
|
||||
// In official version, using ResourceHelper.GetSkin
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<system:String x:Key="BackgroundImageStretchModeFill">Fill</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniform">Uniform (Fit)</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniformToFill">Uniform to Fill (Cover)</system:String>
|
||||
<system:String x:Key="BackgroundMonet">Monet Color</system:String>
|
||||
<system:String x:Key="BackgroundMonetMode">Color Mode</system:String>
|
||||
<system:String x:Key="BackgroundMonetAuto">Auto Extract</system:String>
|
||||
<system:String x:Key="BackgroundMonetCustom">Custom Color</system:String>
|
||||
<system:String x:Key="BackgroundMonetSelectColor">Select Color</system:String>
|
||||
<system:String x:Key="BackgroundMonetAutoTip">Extract dominant color from background image to generate theme palette</system:String>
|
||||
<system:String x:Key="UiTheme">UI Theme</system:String>
|
||||
<system:String x:Key="HotKeySettings">HotKeys</system:String>
|
||||
<system:String x:Key="UpdateSettings">Update</system:String>
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<system:String x:Key="BackgroundImageStretchModeFill">引き伸ばし(フィル)</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniform">アスペクト比保持(フィット)</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniformToFill">アスペクト比保持(トリミング)</system:String>
|
||||
<system:String x:Key="BackgroundMonet">モネ採色</system:String>
|
||||
<system:String x:Key="BackgroundMonetMode">採色モード</system:String>
|
||||
<system:String x:Key="BackgroundMonetAuto">自動採色</system:String>
|
||||
<system:String x:Key="BackgroundMonetCustom">カスタムカラー</system:String>
|
||||
<system:String x:Key="BackgroundMonetSelectColor">色を選択</system:String>
|
||||
<system:String x:Key="BackgroundMonetAutoTip">背景画像からメインカラーを自動抽出してテーマ配色を生成します</system:String>
|
||||
<system:String x:Key="UiTheme">UI主題</system:String>
|
||||
<system:String x:Key="HotKeySettings">ホットキー設定</system:String>
|
||||
<system:String x:Key="UpdateSettings">アップデート設定</system:String>
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
<system:String x:Key="BackgroundImageStretchModeFill">늘려 채움</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniform">비율 유지 (맞춤)</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniformToFill">비율 유지 (자르기)</system:String>
|
||||
<system:String x:Key="BackgroundMonet">모네 색상</system:String>
|
||||
<system:String x:Key="BackgroundMonetMode">색상 모드</system:String>
|
||||
<system:String x:Key="BackgroundMonetAuto">자동 추출</system:String>
|
||||
<system:String x:Key="BackgroundMonetCustom">사용자 지정 색상</system:String>
|
||||
<system:String x:Key="BackgroundMonetSelectColor">색상 선택</system:String>
|
||||
<system:String x:Key="BackgroundMonetAutoTip">배경 이미지에서 주요 색상을 추출하여 테마 색상을 생성합니다</system:String>
|
||||
<system:String x:Key="HotKeySettings">단축키 설정</system:String>
|
||||
<system:String x:Key="UpdateSettings">업데이트</system:String>
|
||||
<system:String x:Key="AchievementSettings">도전과제</system:String>
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<system:String x:Key="BackgroundImageStretchModeFill">拉伸填充</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniform">等比适应</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniformToFill">等比填充(裁剪)</system:String>
|
||||
<system:String x:Key="BackgroundMonet">莫奈取色</system:String>
|
||||
<system:String x:Key="BackgroundMonetMode">取色模式</system:String>
|
||||
<system:String x:Key="BackgroundMonetAuto">自动取色</system:String>
|
||||
<system:String x:Key="BackgroundMonetCustom">自定义颜色</system:String>
|
||||
<system:String x:Key="BackgroundMonetSelectColor">选择颜色</system:String>
|
||||
<system:String x:Key="BackgroundMonetAutoTip">将从背景图自动提取主色生成主题配色</system:String>
|
||||
<system:String x:Key="UiTheme">界面主题</system:String>
|
||||
<system:String x:Key="HotKeySettings">热键设置</system:String>
|
||||
<system:String x:Key="UpdateSettings">更新设置</system:String>
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<system:String x:Key="BackgroundImageStretchModeFill">非等比例填滿</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniform">等比例縮放</system:String>
|
||||
<system:String x:Key="BackgroundImageStretchModeUniformToFill">等比例填滿(裁剪)</system:String>
|
||||
<system:String x:Key="BackgroundMonet">莫奈取色</system:String>
|
||||
<system:String x:Key="BackgroundMonetMode">取色模式</system:String>
|
||||
<system:String x:Key="BackgroundMonetAuto">自動取色</system:String>
|
||||
<system:String x:Key="BackgroundMonetCustom">自訂顏色</system:String>
|
||||
<system:String x:Key="BackgroundMonetSelectColor">選擇顏色</system:String>
|
||||
<system:String x:Key="BackgroundMonetAutoTip">將從背景圖自動提取主色產生主題配色</system:String>
|
||||
<system:String x:Key="UiTheme">介面主題</system:String>
|
||||
<system:String x:Key="HotKeySettings">熱鍵設定</system:String>
|
||||
<system:String x:Key="UpdateSettings">更新設定</system:String>
|
||||
|
||||
@@ -359,6 +359,9 @@ public class SettingsViewModel : Screen
|
||||
|
||||
GuiSettings.LanguageList = languageList;
|
||||
GuiSettings.SwitchDarkMode();
|
||||
|
||||
// 主题初始化完成后,若莫奈取色已开启,恢复调色板(必须在主题切换之后执行)
|
||||
BackgroundSettings.UpdateMonet();
|
||||
}
|
||||
|
||||
private void InitConnectConfig()
|
||||
|
||||
@@ -13,27 +13,53 @@
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools;
|
||||
using MaaWpfGui.Configuration.Factory;
|
||||
using MaaWpfGui.Constants;
|
||||
using MaaWpfGui.Helper;
|
||||
using MaaWpfGui.Utilities;
|
||||
using MaaWpfGui.Utilities.ValueType;
|
||||
using Microsoft.Win32;
|
||||
using Serilog;
|
||||
using Stylet;
|
||||
using MonetModeType = MaaWpfGui.Configuration.Global.GUI.MonetModeType;
|
||||
|
||||
namespace MaaWpfGui.ViewModels.UserControl.Settings;
|
||||
|
||||
public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<BackgroundSettingsUserControlModel>();
|
||||
|
||||
/// <summary>
|
||||
/// 莫奈取色更新的防抖延迟(毫秒)。
|
||||
/// </summary>
|
||||
private const int MonetDebounceMs = 150;
|
||||
|
||||
static BackgroundSettingsUserControlModel()
|
||||
{
|
||||
Instance = new();
|
||||
}
|
||||
|
||||
private BackgroundSettingsUserControlModel()
|
||||
{
|
||||
PropertyDependsOnUtility.InitializePropertyDependencies(this);
|
||||
LocalizationHelper.LanguageChanged += RefreshLocalization;
|
||||
}
|
||||
|
||||
public void RefreshLocalization()
|
||||
{
|
||||
BackgroundImageStretchModeList.RefreshLocalization();
|
||||
BackgroundMonetModeList.RefreshLocalization();
|
||||
}
|
||||
|
||||
public static BackgroundSettingsUserControlModel Instance { get; }
|
||||
|
||||
private static string _backgroundImagePath = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.BackgroundImagePath, "background/background.png");
|
||||
@@ -46,6 +72,9 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.BackgroundImagePath, value);
|
||||
BackgroundImage = RefreshBackgroundImage(value);
|
||||
|
||||
// 背景图变更后,若莫奈自动取色开启,重新提取主色
|
||||
UpdateMonet();
|
||||
|
||||
AchievementTrackerHelper.Instance.Unlock(AchievementIds.CustomizationMaster);
|
||||
}
|
||||
}
|
||||
@@ -83,13 +112,11 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
}
|
||||
}
|
||||
|
||||
public static List<CombinedData> BackgroundImageStretchModeList { get; } =
|
||||
[
|
||||
new() { Display = LocalizationHelper.GetString("BackgroundImageStretchModeNone"), Value = Stretch.None.ToString() },
|
||||
new() { Display = LocalizationHelper.GetString("BackgroundImageStretchModeFill"), Value = Stretch.Fill.ToString() },
|
||||
new() { Display = LocalizationHelper.GetString("BackgroundImageStretchModeUniform"), Value = Stretch.Uniform.ToString() },
|
||||
new() { Display = LocalizationHelper.GetString("BackgroundImageStretchModeUniformToFill"), Value = Stretch.UniformToFill.ToString() },
|
||||
];
|
||||
public LocalizedObservableList<Stretch> BackgroundImageStretchModeList { get; } = new(
|
||||
(Stretch.None, "BackgroundImageStretchModeNone"),
|
||||
(Stretch.Fill, "BackgroundImageStretchModeFill"),
|
||||
(Stretch.Uniform, "BackgroundImageStretchModeUniform"),
|
||||
(Stretch.UniformToFill, "BackgroundImageStretchModeUniformToFill"));
|
||||
|
||||
private static BitmapImage? RefreshBackgroundImage(string imagePath)
|
||||
{
|
||||
@@ -124,6 +151,13 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
get => _backgroundOpacity;
|
||||
set {
|
||||
SetAndNotify(ref _backgroundOpacity, value);
|
||||
|
||||
// 透明度变化后,若莫奈取色开启,重新生成调色板(文字色需根据新的有效背景明度选取)
|
||||
// 滑块拖动会高频触发,使用防抖避免阻塞 UI
|
||||
if (BackgroundMonetEnabled)
|
||||
{
|
||||
ScheduleMonetUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,4 +181,209 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.BackgroundOpacity, BackgroundOpacity.ToString(CultureInfo.InvariantCulture));
|
||||
ConfigurationHelper.SetGlobalValue(ConfigurationKeys.BackgroundBlurEffectRadius, BackgroundBlurEffectRadius.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
#region 莫奈取色(Monet Color)
|
||||
|
||||
/// <summary>
|
||||
/// 莫奈取色是否启用。
|
||||
/// </summary>
|
||||
public bool BackgroundMonetEnabled
|
||||
{
|
||||
get => ConfigFactory.Root.GUI.BackgroundMonetEnabled;
|
||||
set {
|
||||
ConfigFactory.Root.GUI.BackgroundMonetEnabled = value;
|
||||
NotifyOfPropertyChange();
|
||||
UpdateMonet();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 莫奈取色模式(Auto / Custom)。
|
||||
/// </summary>
|
||||
public MonetModeType BackgroundMonetMode
|
||||
{
|
||||
get => ConfigFactory.Root.GUI.BackgroundMonetMode;
|
||||
set {
|
||||
ConfigFactory.Root.GUI.BackgroundMonetMode = value;
|
||||
NotifyOfPropertyChange();
|
||||
UpdateMonet();
|
||||
}
|
||||
}
|
||||
|
||||
[PropertyDependsOn(nameof(BackgroundMonetMode))]
|
||||
public bool IsBackgroundMonetAuto => BackgroundMonetMode == MonetModeType.Auto;
|
||||
|
||||
[PropertyDependsOn(nameof(BackgroundMonetMode))]
|
||||
public bool IsBackgroundMonetCustom => BackgroundMonetMode == MonetModeType.Custom;
|
||||
|
||||
/// <summary>
|
||||
/// 莫奈取色模式下拉列表。
|
||||
/// </summary>
|
||||
public LocalizedObservableList<MonetModeType> BackgroundMonetModeList { get; } = new(
|
||||
(MonetModeType.Auto, "BackgroundMonetAuto"),
|
||||
(MonetModeType.Custom, "BackgroundMonetCustom"));
|
||||
|
||||
/// <summary>
|
||||
/// 用户自定义颜色(HEX 字符串)。
|
||||
/// </summary>
|
||||
public string BackgroundMonetCustomColor
|
||||
{
|
||||
get => ConfigFactory.Root.GUI.BackgroundMonetCustomColor;
|
||||
set {
|
||||
ConfigFactory.Root.GUI.BackgroundMonetCustomColor = value;
|
||||
NotifyOfPropertyChange();
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前生效的莫奈主色,用于色块预览。
|
||||
/// </summary>
|
||||
public Color CurrentMonetColor
|
||||
{
|
||||
get {
|
||||
if (!BackgroundMonetEnabled)
|
||||
{
|
||||
// 关闭时显示默认主色
|
||||
return ThemeHelper.String2Color("#326CF3");
|
||||
}
|
||||
|
||||
return IsBackgroundMonetCustom
|
||||
? ThemeHelper.String2Color(BackgroundMonetCustomColor)
|
||||
: _lastExtractedColor ?? ThemeHelper.String2Color("#326CF3");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上次自动提取到的颜色(缓存)。
|
||||
/// </summary>
|
||||
private Color? _lastExtractedColor;
|
||||
|
||||
/// <summary>
|
||||
/// 打开 HandyControl ColorPicker 弹窗让用户选择自定义颜色。
|
||||
/// </summary>
|
||||
public void SelectMonetColor()
|
||||
{
|
||||
var picker = SingleOpenHelper.CreateControl<ColorPicker>();
|
||||
|
||||
// 初始化 ColorPicker 为当前颜色
|
||||
var current = ThemeHelper.String2Color(BackgroundMonetCustomColor);
|
||||
picker.SelectedBrush = new SolidColorBrush(current);
|
||||
|
||||
var dialog = Dialog.Show(picker, nameof(Views.UI.RootView));
|
||||
|
||||
picker.Confirmed += (_, e) => {
|
||||
var color = e.Info;
|
||||
BackgroundMonetCustomColor = ThemeHelper.Color2HexString(color);
|
||||
UpdateMonet();
|
||||
dialog.Close();
|
||||
};
|
||||
|
||||
picker.Canceled += (_, _) => dialog.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 防抖取消令牌源,避免滑块拖动时高频重新计算调色板。
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _monetUpdateCts;
|
||||
|
||||
/// <summary>
|
||||
/// 调度一次莫奈取色更新(防抖 150ms)。
|
||||
/// 滑块拖动等高频场景调用此方法,而非直接 <see cref="UpdateMonet"/>。
|
||||
/// </summary>
|
||||
private void ScheduleMonetUpdate()
|
||||
{
|
||||
_monetUpdateCts?.Cancel();
|
||||
_monetUpdateCts = new CancellationTokenSource();
|
||||
_ = UpdateMonetAsync(_monetUpdateCts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前配置执行莫奈取色逻辑。
|
||||
/// 自动 / 自定义模式都会将计算放到后台线程,仅资源写入在 UI 线程。
|
||||
/// </summary>
|
||||
public void UpdateMonet()
|
||||
{
|
||||
_monetUpdateCts?.Cancel();
|
||||
_ = UpdateMonetAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行莫奈取色,支持取消(用于防抖)。
|
||||
/// 所有异常均在方法内部捕获并记录,避免 fire-and-forget 调用产生未观察异常。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
private async Task UpdateMonetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 防抖延迟:等待 150ms,期间若被取消则直接返回
|
||||
try
|
||||
{
|
||||
await Task.Delay(MonetDebounceMs, cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!BackgroundMonetEnabled)
|
||||
{
|
||||
ThemeHelper.RevertMonetPalette();
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
return;
|
||||
}
|
||||
|
||||
if (BackgroundMonetMode == MonetModeType.Custom)
|
||||
{
|
||||
// 自定义模式:直接用用户选定的颜色,计算放后台线程
|
||||
var color = ThemeHelper.String2Color(BackgroundMonetCustomColor);
|
||||
await ThemeHelper.ApplyMonetPaletteAsync(color, BackgroundOpacity, cancellationToken);
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 自动模式:从背景图提取主色(本身就是异步的)
|
||||
await UpdateMonetAutoAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常的防抖取消,静默忽略
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 兜底:fire-and-forget 调用中的异常不能逃逸,记录后忽略
|
||||
_logger.Error(ex, "Failed to update Monet palette.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行自动取色:提取主色 → 应用调色板。
|
||||
/// 颜色提取和调色板计算在后台线程,资源写入在 UI 线程。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
private async Task UpdateMonetAutoAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var image = BackgroundImage;
|
||||
if (image == null)
|
||||
{
|
||||
// 没有背景图时,取色无意义,先回退
|
||||
ThemeHelper.RevertMonetPalette();
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
return;
|
||||
}
|
||||
|
||||
var rawColor = await ColorExtractorHelper.ExtractDominantColorAsync(image).ConfigureAwait(true);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 直接使用提取到的原始主色生成调色板
|
||||
_lastExtractedColor = rawColor;
|
||||
|
||||
await ThemeHelper.ApplyMonetPaletteAsync(rawColor, BackgroundOpacity, cancellationToken);
|
||||
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
}
|
||||
|
||||
#endregion 莫奈取色(Monet Color)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
xmlns:settings_vms="clr-namespace:MaaWpfGui.ViewModels.UserControl.Settings"
|
||||
d:Background="White"
|
||||
d:DataContext="{d:DesignInstance {x:Type settings_vms:BackgroundSettingsUserControlModel}}"
|
||||
d:DesignHeight="300"
|
||||
d:DesignHeight="350"
|
||||
d:DesignWidth="550"
|
||||
s:View.ActionTarget="{Binding}"
|
||||
mc:Ignorable="d">
|
||||
<StackPanel>
|
||||
<StackPanel Margin="10">
|
||||
<StackPanel Margin="5">
|
||||
<controls:TextBlock
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -40,7 +40,7 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<controls:TextBlock
|
||||
@@ -60,7 +60,7 @@
|
||||
</hc:PreviewSlider>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<controls:TextBlock
|
||||
@@ -80,7 +80,7 @@
|
||||
</hc:PreviewSlider>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<controls:TextBlock
|
||||
@@ -93,9 +93,67 @@
|
||||
DisplayMemberPath="Display"
|
||||
IsEditable="True"
|
||||
IsEnabled="True"
|
||||
ItemsSource="{c:Binding BackgroundImageStretchModeList}"
|
||||
SelectedValue="{c:Binding BackgroundImageStretchMode}"
|
||||
ItemsSource="{Binding BackgroundImageStretchModeList}"
|
||||
SelectedValue="{Binding BackgroundImageStretchMode}"
|
||||
SelectedValuePath="Value" />
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<CheckBox
|
||||
Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
Content="{DynamicResource BackgroundMonet}"
|
||||
IsChecked="{Binding BackgroundMonetEnabled}" />
|
||||
<StackPanel
|
||||
Margin="5"
|
||||
IsEnabled="{Binding BackgroundMonetEnabled}"
|
||||
Visibility="{c:Binding BackgroundMonetEnabled}">
|
||||
<!-- 取色模式选择 -->
|
||||
<controls:TextBlock
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{DynamicResource BackgroundMonetMode}" />
|
||||
<hc:ComboBox
|
||||
Width="200"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
DisplayMemberPath="Display"
|
||||
ItemsSource="{Binding BackgroundMonetModeList}"
|
||||
SelectedValue="{Binding BackgroundMonetMode}"
|
||||
SelectedValuePath="Value" />
|
||||
|
||||
<!-- 自动模式提示 -->
|
||||
<controls:TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="11"
|
||||
Opacity="0.6"
|
||||
Text="{DynamicResource BackgroundMonetAutoTip}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{c:Binding IsBackgroundMonetAuto}" />
|
||||
|
||||
<!-- 自定义颜色选择 -->
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{c:Binding IsBackgroundMonetCustom}">
|
||||
<!-- 色块预览 -->
|
||||
<Rectangle
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
RadiusX="4"
|
||||
RadiusY="4"
|
||||
Stroke="{DynamicResource RegionBrushOpacity50}">
|
||||
<Rectangle.Fill>
|
||||
<SolidColorBrush d:Color="{DynamicResource PrimaryColor}" Color="{Binding CurrentMonetColor}" />
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Button
|
||||
hc:BorderElement.CornerRadius="4"
|
||||
Command="{s:Action SelectMonetColor}"
|
||||
Content="{DynamicResource BackgroundMonetSelectColor}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user