2025-07-31 20:12:24 +08:00
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using System.Windows.Media.Animation;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
using NeumUI.Extensions;
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
namespace NeumUI.Appearance;
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 允许通过交换包含动态资源的资源字典来管理应用程序主题。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <example>
|
|
|
|
|
|
/// <code lang="csharp"> ThemeManager.SwitchThemeMode();</code>
|
|
|
|
|
|
/// </example>
|
|
|
|
|
|
public class ThemeManager
|
|
|
|
|
|
{
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前程序集的命名空间。
|
|
|
|
|
|
/// 该属性返回一个字符串,表示执行此代码的程序集的名称。此命名空间用于构建资源字典路径或其它需要引用程序集名称的地方。
|
|
|
|
|
|
/// </summary>
|
2025-07-31 20:12:24 +08:00
|
|
|
|
public static string LibraryNamespace => Assembly.GetExecutingAssembly().GetName().Name;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取主题资源字典的路径。
|
|
|
|
|
|
/// 该属性返回一个字符串,表示应用程序中主题资源字典文件的位置。此路径用于加载不同的主题样式文件(如Light.xaml, Dark.xaml等)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static string ThemesDictionaryPath => $"pack://application:,,,/{LibraryNamespace};component/Themes/";
|
|
|
|
|
|
|
|
|
|
|
|
private static ThemeMode _activeThemeMode = ThemeMode.Light;
|
|
|
|
|
|
private static ThemePalette _activeThemePalette = ThemePalette.DaybreakBlue;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
///
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static event Action<ThemeMode>? ThemeModeChanged;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
///
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static event Action<ThemePalette>? ThemePaletteChanged;
|
|
|
|
|
|
|
|
|
|
|
|
#region Animation
|
|
|
|
|
|
|
|
|
|
|
|
private static Task AnimateBrushColorAsync(SolidColorBrush brush, Color toColor, int durationMs = 500)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (brush.IsFrozen)
|
|
|
|
|
|
return Task.CompletedTask; // 冻结的Brush无法动画,直接返回完成的Task
|
|
|
|
|
|
|
|
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
|
|
|
|
var anim = new ColorAnimation
|
|
|
|
|
|
{
|
|
|
|
|
|
To = toColor,
|
|
|
|
|
|
Duration = TimeSpan.FromMilliseconds(durationMs),
|
|
|
|
|
|
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut },
|
|
|
|
|
|
FillBehavior = FillBehavior.HoldEnd
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 在动画完成时标记任务完成
|
|
|
|
|
|
anim.Completed += (_, _) => tcs.SetResult(true);
|
|
|
|
|
|
brush.BeginAnimation(SolidColorBrush.ColorProperty, anim);
|
|
|
|
|
|
|
|
|
|
|
|
return tcs.Task;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前设置的应用程序主题。
|
|
|
|
|
|
/// </summary>
|
2025-08-20 12:10:13 +08:00
|
|
|
|
public static ThemeMode GetAppThemeMode()
|
2025-07-31 20:12:24 +08:00
|
|
|
|
{
|
|
|
|
|
|
ResourceDictionaryManager appDictionaries = new();
|
|
|
|
|
|
var themeDictionary = appDictionaries.LookupDictionary("themes");
|
|
|
|
|
|
|
|
|
|
|
|
if (themeDictionary == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return ThemeMode.Light;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var themeUri = themeDictionary.Source.ToString();
|
|
|
|
|
|
|
|
|
|
|
|
if (themeUri.Contains("light", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeThemeMode = ThemeMode.Light;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (themeUri.Contains("dark", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeThemeMode = ThemeMode.Dark;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return _activeThemeMode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将资源应用于 <paramref name="frameworkElement"/>.比如窗口
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void ApplyResourcesToElement(FrameworkElement frameworkElement)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (frameworkElement.Resources.MergedDictionaries.Count <
|
|
|
|
|
|
Current.Resources.MergedDictionaries.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var dictionary in ThemeManager.Current.Resources.MergedDictionaries)
|
|
|
|
|
|
{
|
|
|
|
|
|
frameworkElement.Resources.MergedDictionaries.Add(dictionary);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 应用指定的主题调色板到应用程序。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="themePalette">要应用的主题调色板。</param>
|
|
|
|
|
|
public static void ApplyThemePalette(ThemePalette themePalette)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (themePalette == ThemePalette.DaybreakBlue)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_activeThemePalette = themePalette;
|
|
|
|
|
|
ThemePaletteChanged?.Invoke(themePalette);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 设置应用程序的主题模式。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="themeMode">要设置的主题模式,可以是浅色或深色。</param>
|
|
|
|
|
|
/// <exception cref="ArgumentOutOfRangeException">如果提供的主题模式无效,则抛出此异常。</exception>
|
|
|
|
|
|
public static void SetThemeMode(ThemeMode themeMode)
|
|
|
|
|
|
{
|
|
|
|
|
|
GetAppThemeMode();
|
|
|
|
|
|
if (themeMode == _activeThemeMode)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
_activeThemeMode = themeMode;
|
|
|
|
|
|
var themeDictionaryName = themeMode switch
|
|
|
|
|
|
{
|
|
|
|
|
|
ThemeMode.Light => "Light",
|
|
|
|
|
|
ThemeMode.Dark => "Dark",
|
|
|
|
|
|
_ => throw new ArgumentOutOfRangeException(nameof(themeMode), themeMode, null)
|
|
|
|
|
|
};
|
|
|
|
|
|
ResourceDictionaryManager appDictionaries = new();
|
|
|
|
|
|
if (!appDictionaries.UpdateDictionary(
|
|
|
|
|
|
"themes",
|
|
|
|
|
|
new Uri($"{ThemesDictionaryPath}{themeDictionaryName}.xaml", UriKind.Absolute)))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
ThemeModeChanged?.Invoke(themeMode);
|
|
|
|
|
|
}
|
2025-07-31 20:12:24 +08:00
|
|
|
|
/// <summary>
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// 更改当前应用程序明暗主题。
|
2025-07-31 20:12:24 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void SwitchThemeMode()
|
|
|
|
|
|
{
|
2025-08-20 12:10:13 +08:00
|
|
|
|
GetAppThemeMode();
|
2025-07-31 20:12:24 +08:00
|
|
|
|
|
|
|
|
|
|
var toThemeMode = _activeThemeMode == ThemeMode.Light ? ThemeMode.Dark : ThemeMode.Light;
|
|
|
|
|
|
|
|
|
|
|
|
var themeDictionaryName = toThemeMode switch
|
|
|
|
|
|
{
|
|
|
|
|
|
ThemeMode.Light => "Light",
|
|
|
|
|
|
ThemeMode.Dark => "Dark",
|
|
|
|
|
|
_ => throw new NotImplementedException(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
#region 动画过渡
|
|
|
|
|
|
|
|
|
|
|
|
// // 1. 加载目标主题的资源字典,但先不合并
|
|
|
|
|
|
// ResourceDictionary newTheme = new ResourceDictionary
|
|
|
|
|
|
// {
|
|
|
|
|
|
// Source = new Uri($"{ThemesDictionaryPath}{themeDictionaryName}.xaml", UriKind.Absolute)
|
|
|
|
|
|
// };
|
|
|
|
|
|
// // 2. 遍历新主题中的画刷资源
|
|
|
|
|
|
// foreach (var key in newTheme.Keys)
|
|
|
|
|
|
// {
|
|
|
|
|
|
// // 只处理 SolidColorBrush
|
|
|
|
|
|
// if (newTheme[key] is SolidColorBrush newBrush && Current.Resources[key] is SolidColorBrush currentBrush)
|
|
|
|
|
|
// {
|
|
|
|
|
|
//
|
|
|
|
|
|
// // var fromColor = animatableBrush.Color;
|
|
|
|
|
|
// var toColor = new SolidColorBrush(newBrush.Color).Color;
|
|
|
|
|
|
//
|
|
|
|
|
|
// // 3. 在当前的应用程序资源中找到同名的画刷
|
|
|
|
|
|
// // 4. 创建颜色动画
|
|
|
|
|
|
// var colorAnimation = new ColorAnimation
|
|
|
|
|
|
// {
|
|
|
|
|
|
// // From = fromColor,
|
|
|
|
|
|
// To = toColor,
|
|
|
|
|
|
// Duration = new Duration(TimeSpan.FromMilliseconds(1000)), // 动画时长
|
|
|
|
|
|
// EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseInOut } // 缓动函数,使动画更自然
|
|
|
|
|
|
// };
|
|
|
|
|
|
//
|
|
|
|
|
|
// // 5. 对当前画刷的 Color 属性应用动画
|
|
|
|
|
|
// currentBrush.BeginAnimation(SolidColorBrush.ColorProperty, colorAnimation);
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
ResourceDictionaryManager appDictionaries = new();
|
|
|
|
|
|
if (!appDictionaries.UpdateDictionary(
|
|
|
|
|
|
"themes",
|
|
|
|
|
|
new Uri($"{ThemesDictionaryPath}{themeDictionaryName}.xaml", UriKind.Absolute)))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_activeThemeMode = toThemeMode;
|
|
|
|
|
|
|
|
|
|
|
|
ThemeModeChanged?.Invoke(toThemeMode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region UiApplication
|
|
|
|
|
|
|
|
|
|
|
|
private static ThemeManager? _themeManagerInstance;
|
|
|
|
|
|
|
|
|
|
|
|
private readonly Application? application;
|
|
|
|
|
|
|
|
|
|
|
|
private Window? mainWindow;
|
|
|
|
|
|
|
|
|
|
|
|
private ResourceDictionary? resources;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化 <see cref="ThemeManager"/> 实例.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private ThemeManager(Application? application)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (application is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!ApplicationHasResources(application))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.application = application;
|
|
|
|
|
|
|
|
|
|
|
|
System.Diagnostics.Debug
|
2025-08-12 23:08:54 +08:00
|
|
|
|
.WriteLine($"INFO | {typeof(ThemeManager)} application is {this.application}", ThemeManager.LibraryNamespace);
|
2025-07-31 20:12:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool ApplicationHasResources(Application application)
|
|
|
|
|
|
{
|
|
|
|
|
|
return application
|
|
|
|
|
|
.Resources.MergedDictionaries
|
|
|
|
|
|
.Where(e => e.Source is not null)
|
|
|
|
|
|
.Any(e => e.Source
|
|
|
|
|
|
.ToString()
|
|
|
|
|
|
.Contains(ThemeManager.LibraryNamespace, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将应用程序的 变为 shutdown 模式。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Shutdown()
|
|
|
|
|
|
{
|
|
|
|
|
|
application?.Shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取或设置应用程序的资源。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public object TryFindResource(object resourceKey)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Resources[resourceKey];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前UI应用程序。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static ThemeManager Current
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
_themeManagerInstance ??= new ThemeManager(Application.Current);
|
|
|
|
|
|
|
|
|
|
|
|
return _themeManagerInstance;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取或设置应用程序的主窗口。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public Window? MainWindow
|
|
|
|
|
|
{
|
|
|
|
|
|
get => application?.MainWindow ?? mainWindow;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (application != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
application.MainWindow = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mainWindow = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取或设置应用程序的资源。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public ResourceDictionary Resources
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (resources is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
resources = [];
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var themesDictionary = new ThemesDictionary();
|
|
|
|
|
|
var controlsDictionary = new ControlsDictionary();
|
|
|
|
|
|
resources.MergedDictionaries.Add(themesDictionary);
|
|
|
|
|
|
resources.MergedDictionaries.Add(controlsDictionary);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
// ignored
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return application?.Resources ?? resources;
|
|
|
|
|
|
}
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (application is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
application.Resources = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resources = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|