月更
This commit is contained in:
17
AntDesignWPF/Appearance/ControlsDictionary.cs
Normal file
17
AntDesignWPF/Appearance/ControlsDictionary.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace AntDesignWPF.Appearance;
|
||||
[Localizability(LocalizationCategory.Ignore)]
|
||||
[Ambient]
|
||||
[UsableDuringInitialization(true)]
|
||||
public class ControlsDictionary : ResourceDictionary
|
||||
{
|
||||
private string DictionaryUri => $"pack://application:,,,/{Assembly.GetExecutingAssembly().GetName().Name};component/Themes/Generic.xaml";
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个新实例<see cref="ControlsDictionary"/>类。 控件字典的默认构造函数定义<see cref="ResourceDictionary.Source"/>
|
||||
/// </summary>
|
||||
public ControlsDictionary() { Source = new Uri(DictionaryUri, UriKind.Absolute); }
|
||||
}
|
||||
137
AntDesignWPF/Appearance/ResourceDictionaryManager.cs
Normal file
137
AntDesignWPF/Appearance/ResourceDictionaryManager.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using AntDesignWPF.Extensions;
|
||||
|
||||
|
||||
namespace AntDesignWPF.Appearance;
|
||||
|
||||
/// <summary>
|
||||
/// 管理资源字典
|
||||
/// </summary>
|
||||
internal class ResourceDictionaryManager
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 获取命名空间,例如正在搜索的资源库。
|
||||
/// </summary>
|
||||
private string SearchNamespace { get; } = Assembly.GetExecutingAssembly().GetName().Name;
|
||||
|
||||
/// <summary>
|
||||
/// 查找资源 <see cref="ResourceDictionary"/>
|
||||
/// </summary>
|
||||
/// <param name="resourceLookup">任何部分的资源名称。</param>
|
||||
/// <returns><see cref="ResourceDictionary"/>, <see langword="null"/>如果不存在,返回null</returns>
|
||||
public ResourceDictionary? LookupDictionary(string resourceLookup)
|
||||
{
|
||||
Collection<ResourceDictionary> applicationDictionaries = ThemeManager.Current.Resources.MergedDictionaries;
|
||||
|
||||
if (applicationDictionaries.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (ResourceDictionary t in applicationDictionaries)
|
||||
{
|
||||
string resourceDictionaryUri;
|
||||
|
||||
if (t?.Source != null)
|
||||
{
|
||||
resourceDictionaryUri = t.Source.ToString();
|
||||
if (resourceDictionaryUri.Contains(SearchNamespace, StringComparison.OrdinalIgnoreCase) &&
|
||||
resourceDictionaryUri.Contains(resourceLookup, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ResourceDictionary? t1 in t!.MergedDictionaries)
|
||||
{
|
||||
if (t1?.Source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
resourceDictionaryUri = t1.Source.ToString();
|
||||
|
||||
if (resourceDictionaryUri.Contains(SearchNamespace, StringComparison.OrdinalIgnoreCase)&&
|
||||
resourceDictionaryUri.Contains(resourceLookup, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return t1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新应用程序中的资源字典
|
||||
/// </summary>
|
||||
/// <param name="resourceLookup">要查找的资源名称的一部分</param>
|
||||
/// <param name="newResourceUri">用于替换的新资源的有效Uri</param>
|
||||
/// <returns>如果字典Uri已更新则返回true,否则返回false</returns>
|
||||
public bool UpdateDictionary(string resourceLookup, Uri? newResourceUri)
|
||||
{
|
||||
// 获取应用程序的合并资源字典集合
|
||||
var applicationDictionaries = ThemeManager
|
||||
.Current
|
||||
.Resources
|
||||
.MergedDictionaries;
|
||||
|
||||
// 如果没有资源字典或新的Uri为空,则返回false
|
||||
if (applicationDictionaries.Count == 0 || newResourceUri is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 遍历顶层资源字典
|
||||
for (var i = 0; i < applicationDictionaries.Count; i++)
|
||||
{
|
||||
string sourceUri;
|
||||
|
||||
// 检查当前资源字典是否有Source
|
||||
if (applicationDictionaries[i]?.Source != null)
|
||||
{
|
||||
sourceUri = applicationDictionaries[i].Source.ToString();
|
||||
|
||||
// 检查资源Uri是否匹配搜索条件
|
||||
if (sourceUri.Contains(SearchNamespace, StringComparison.OrdinalIgnoreCase) &&
|
||||
sourceUri.Contains(resourceLookup, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 使用新的Uri更新资源字典
|
||||
applicationDictionaries[i] = new() { Source = newResourceUri };
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历合并的子资源字典
|
||||
for (var j = 0; j < applicationDictionaries[i].MergedDictionaries.Count; j++)
|
||||
{
|
||||
if (applicationDictionaries[i].MergedDictionaries[j]?.Source == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceUri = applicationDictionaries[i].MergedDictionaries[j].Source.ToString();
|
||||
|
||||
// 检查子资源字典Uri是否匹配搜索条件
|
||||
if (sourceUri.Contains(SearchNamespace, StringComparison.OrdinalIgnoreCase) &&
|
||||
sourceUri.Contains(resourceLookup, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 使用新的Uri更新子资源字典
|
||||
applicationDictionaries[i].MergedDictionaries[j] = new ResourceDictionary { Source = newResourceUri };
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 未找到匹配的资源字典,返回false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
295
AntDesignWPF/Appearance/ThemeManager.cs
Normal file
295
AntDesignWPF/Appearance/ThemeManager.cs
Normal file
@@ -0,0 +1,295 @@
|
||||
using System.Reflection;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
using AntDesignWPF.Extensions;
|
||||
|
||||
namespace AntDesignWPF.Appearance;
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过交换包含动态资源的资源字典来管理应用程序主题。
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code lang="csharp"> ThemeManager.ChangeAppTheme( ThemeMode.Light );</code> <code lang="csharp"> if
|
||||
/// (ThemeManager.GetAppTheme() == ThemeMode.Dark) { ThemeManager.ChangeAppTheme(
|
||||
/// ThemeMode.Light ); }</code> <code> ThemeManager.Changed += (theme, accent) => {
|
||||
/// Debug.WriteLine($"Application theme changed to {theme.ToString()}"); };</code>
|
||||
/// </example>
|
||||
public class ThemeManager
|
||||
{
|
||||
internal static string LibraryNamespace => Assembly.GetExecutingAssembly().GetName().Name;
|
||||
|
||||
public static string ThemesDictionaryPath => $"pack://application:,,,/{LibraryNamespace};component/Themes/";
|
||||
|
||||
private static ThemeMode _activeThemeMode = ThemeMode.Light;
|
||||
private static ThemePalette _activeThemePalette = ThemePalette.DaybreakBlue;
|
||||
public static event Action<ThemeMode>? ThemeModeChanged;
|
||||
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 += (s, e) => tcs.SetResult(true);
|
||||
brush.BeginAnimation(SolidColorBrush.ColorProperty, anim);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
/// <summary>
|
||||
/// 动画过渡
|
||||
/// </summary>
|
||||
/// <param name="themeUri"></param>
|
||||
public void ChangeTheme(Uri themeUri)
|
||||
{
|
||||
// 1. 加载目标主题的资源字典,但先不合并
|
||||
ResourceDictionary newTheme = new ResourceDictionary { Source = themeUri };
|
||||
|
||||
// 2. 遍历新主题中的画刷资源
|
||||
foreach (var key in newTheme.Keys)
|
||||
{
|
||||
// 只处理 SolidColorBrush
|
||||
if (newTheme[key] is SolidColorBrush newBrush)
|
||||
{
|
||||
// 3. 在当前的应用程序资源中找到同名的画刷
|
||||
var currentBrush = Application.Current.Resources[key] as SolidColorBrush;
|
||||
if (currentBrush != null)
|
||||
{
|
||||
// 4. 创建颜色动画
|
||||
var colorAnimation = new ColorAnimation
|
||||
{
|
||||
From = currentBrush.Color,
|
||||
To = newBrush.Color,
|
||||
Duration = new Duration(TimeSpan.FromMilliseconds(300)), // 动画时长
|
||||
EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseInOut } // 缓动函数,使动画更自然
|
||||
};
|
||||
|
||||
// 5. 对当前画刷的 Color 属性应用动画
|
||||
currentBrush.BeginAnimation(SolidColorBrush.ColorProperty, colorAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 将资源应用于 <paramref name="frameworkElement"/>.
|
||||
/// </summary>
|
||||
public static void ApplyResourcesToElement(FrameworkElement frameworkElement)
|
||||
{
|
||||
if (frameworkElement.Resources.MergedDictionaries.Count <
|
||||
ThemeManager.Current.Resources.MergedDictionaries.Count)
|
||||
{
|
||||
foreach (var dictionary in ThemeManager.Current.Resources.MergedDictionaries)
|
||||
{
|
||||
frameworkElement.Resources.MergedDictionaries.Add(dictionary);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void SwitchThemePalette(ThemePalette themePalette)
|
||||
{
|
||||
if (themePalette == ThemePalette.DaybreakBlue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 更改当前应用程序主题。
|
||||
/// </summary>
|
||||
/// <param name="themeMode">要设置的主题</param>
|
||||
/// <param name="backgroundEffect">是否应用自定义背景效果。</param>
|
||||
/// <param name="updateAccent">是否应更改颜色强调。</param>
|
||||
public static void SwitchThemeMode(ThemeMode themeMode)
|
||||
{
|
||||
if (themeMode == _activeThemeMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceDictionaryManager appDictionaries = new();
|
||||
|
||||
var themeDictionaryName = themeMode switch
|
||||
{
|
||||
ThemeMode.Light => "Light",
|
||||
ThemeMode.Dark => "Dark",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
//ApplyThemeWithAnimation(applicationTheme);
|
||||
if (!appDictionaries.UpdateDictionary(
|
||||
"themes",
|
||||
new Uri($"{ThemesDictionaryPath}{themeDictionaryName}.xaml", UriKind.Absolute)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeThemeMode = themeMode;
|
||||
|
||||
ThemeModeChanged?.Invoke(themeMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前设置的应用程序主题。
|
||||
/// </summary>
|
||||
/// <returns><see cref="ThemeMode.Unknow"/> if something goes wrong.</returns>
|
||||
public static ThemeMode GetAppTheme()
|
||||
{
|
||||
if (_activeThemeMode == ThemeMode.Light)
|
||||
{
|
||||
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;
|
||||
}
|
||||
#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
|
||||
.WriteLine($"INFO | {typeof(ThemeManager)} application is {this.application}", "AntDesignWPF");
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return application?.Resources ?? resources;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (application is not null)
|
||||
{
|
||||
application.Resources = value;
|
||||
}
|
||||
|
||||
resources = value;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
40
AntDesignWPF/Appearance/ThemeMode.cs
Normal file
40
AntDesignWPF/Appearance/ThemeMode.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AntDesignWPF.Appearance;
|
||||
public enum ThemeMode
|
||||
{
|
||||
Light,
|
||||
Dark
|
||||
}
|
||||
public enum ThemePalette
|
||||
{
|
||||
[Description("薄暮红")]
|
||||
DustRed,
|
||||
[Description("火山红")]
|
||||
Volcano,
|
||||
[Description("日落橙")]
|
||||
SunsetOrange,
|
||||
[Description("金盏花")]
|
||||
CalendulaGold,
|
||||
[Description("日出黄")]
|
||||
SunriseYellow,
|
||||
[Description("青柠绿")]
|
||||
Lime,
|
||||
[Description("极光绿")]
|
||||
PolarGreen,
|
||||
[Description("明青")]
|
||||
Cyan,
|
||||
[Description("拂晓蓝")]
|
||||
DaybreakBlue,
|
||||
[Description("极客蓝")]
|
||||
GeekBlue,
|
||||
[Description("酱紫")]
|
||||
GoldenPurple,
|
||||
[Description("法式洋红")]
|
||||
Magenta,
|
||||
}
|
||||
32
AntDesignWPF/Appearance/ThemesDictionary.cs
Normal file
32
AntDesignWPF/Appearance/ThemesDictionary.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Windows.Markup;
|
||||
|
||||
|
||||
namespace AntDesignWPF.Appearance;
|
||||
|
||||
/// <summary>
|
||||
/// 提供一个字典实现,其中包含组件和其他设备使用的主题资源。
|
||||
/// </summary>
|
||||
[Localizability(LocalizationCategory.Ignore)]
|
||||
[Ambient]
|
||||
[UsableDuringInitialization(true)]
|
||||
public class ThemesDictionary : ResourceDictionary
|
||||
{
|
||||
public ThemesDictionary() { SetSourceBasedOnSelectedTheme(ThemeMode.Light); }
|
||||
|
||||
private void SetSourceBasedOnSelectedTheme(ThemeMode? selectedApplicationTheme)
|
||||
{
|
||||
var themeName = selectedApplicationTheme switch
|
||||
{
|
||||
ThemeMode.Light => "Light",
|
||||
ThemeMode.Dark => "Dark",
|
||||
_ => "Light",
|
||||
};
|
||||
|
||||
Source = new Uri($"{ThemeManager.ThemesDictionaryPath}{themeName}.xaml", UriKind.Absolute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认应用程序主题。
|
||||
/// </summary>
|
||||
public ThemeMode Theme { set => SetSourceBasedOnSelectedTheme(value); }
|
||||
}
|
||||
Reference in New Issue
Block a user