332 lines
9.9 KiB
C#
332 lines
9.9 KiB
C#
|
|
using System.Reflection;
|
|||
|
|
using System.Windows.Controls;
|
|||
|
|
using System.Windows.Media.Animation;
|
|||
|
|
using System.Windows.Shapes;
|
|||
|
|
|
|||
|
|
using WPFluent.Controls;
|
|||
|
|
using WPFluent.Extensions;
|
|||
|
|
using WPFluent.Markup;
|
|||
|
|
|
|||
|
|
namespace WPFluent.Appearance;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 允许通过交换包含动态资源的资源字典来管理应用程序主题。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <example>
|
|||
|
|
/// <code lang="csharp"> ThemeManager.ChangeAppTheme( ThemeType.Light );</code> <code lang="csharp"> if
|
|||
|
|
/// (ThemeManager.GetAppTheme() == ThemeType.Dark) { ThemeManager.ChangeAppTheme(
|
|||
|
|
/// ThemeType.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 ThemeType _activeApplicationTheme = ThemeType.Unknow;
|
|||
|
|
public static event Action<ThemeType, Color>? Changed;
|
|||
|
|
|
|||
|
|
#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>
|
|||
|
|
private static async void ApplyThemeWithAnimation(ThemeType theme)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var window = Application.Current.MainWindow;
|
|||
|
|
if (window == null) return;
|
|||
|
|
// 确保内容容器支持 Children
|
|||
|
|
if (!(window.Content is Panel panel))
|
|||
|
|
throw new InvalidOperationException("ThemeManager requires Window.Content to be a Panel.");
|
|||
|
|
|
|||
|
|
// 创建遮罩
|
|||
|
|
var overlay = new Rectangle
|
|||
|
|
{
|
|||
|
|
Fill = new SolidColorBrush(Colors.Black),
|
|||
|
|
Opacity = 0,
|
|||
|
|
IsHitTestVisible = false,
|
|||
|
|
};
|
|||
|
|
Panel.SetZIndex(overlay, int.MaxValue);
|
|||
|
|
panel.Children.Add(overlay);
|
|||
|
|
|
|||
|
|
// 淡入
|
|||
|
|
await AnimateOpacity(overlay, 0, 1, TimeSpan.FromMilliseconds(300));
|
|||
|
|
|
|||
|
|
// 切换资源字典
|
|||
|
|
//ApplyThemeInternal(theme);
|
|||
|
|
ChangeAppTheme(theme);
|
|||
|
|
|
|||
|
|
// 淡出
|
|||
|
|
await AnimateOpacity(overlay, 1, 0, TimeSpan.FromMilliseconds(300));
|
|||
|
|
panel.Children.Remove(overlay);
|
|||
|
|
|
|||
|
|
Changed?.Invoke(theme, AccentColorManager.SystemAccent);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static Task AnimateOpacity(UIElement element, double from, double to, TimeSpan duration)
|
|||
|
|
{
|
|||
|
|
var tcs = new TaskCompletionSource<bool>();
|
|||
|
|
var anim = new DoubleAnimation(from, to, new Duration(duration));
|
|||
|
|
anim.Completed += (s, e) => tcs.SetResult(true);
|
|||
|
|
element.BeginAnimation(UIElement.OpacityProperty, anim);
|
|||
|
|
return tcs.Task;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 更改当前应用程序主题。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="applicationTheme">要设置的主题</param>
|
|||
|
|
/// <param name="backgroundEffect">是否应用自定义背景效果。</param>
|
|||
|
|
/// <param name="updateAccent">是否应更改颜色强调。</param>
|
|||
|
|
public static void ChangeAppTheme(ThemeType applicationTheme, WindowBackdropType backgroundEffect = WindowBackdropType.Mica,
|
|||
|
|
bool updateAccent = true)
|
|||
|
|
{
|
|||
|
|
if (updateAccent)
|
|||
|
|
{
|
|||
|
|
//TODO
|
|||
|
|
AccentColorManager.Apply(
|
|||
|
|
Color.FromRgb(194, 115, 37),
|
|||
|
|
applicationTheme,
|
|||
|
|
false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (applicationTheme == ThemeType.Unknow)
|
|||
|
|
{
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ResourceDictionaryManager appDictionaries = new();
|
|||
|
|
|
|||
|
|
var themeDictionaryName = applicationTheme switch
|
|||
|
|
{
|
|||
|
|
ThemeType.Light => "Light",
|
|||
|
|
ThemeType.Dark => "Dark",
|
|||
|
|
ThemeType.HcWhite => "HCWhite",
|
|||
|
|
ThemeType.HcBlack => "HCBlack",
|
|||
|
|
_ => "Light"
|
|||
|
|
};
|
|||
|
|
//ApplyThemeWithAnimation(applicationTheme);
|
|||
|
|
if (!appDictionaries.UpdateDictionary(
|
|||
|
|
"themes",
|
|||
|
|
new Uri($"{ThemesDictionaryPath}{themeDictionaryName}.xaml", UriKind.Absolute)))
|
|||
|
|
{
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_activeApplicationTheme = applicationTheme;
|
|||
|
|
|
|||
|
|
Changed?.Invoke(applicationTheme, AccentColorManager.SystemAccent);
|
|||
|
|
|
|||
|
|
if (Current.MainWindow is Window mainWindow)
|
|||
|
|
{
|
|||
|
|
WindowBackgroundManager.UpdateBackground(mainWindow, applicationTheme, backgroundEffect);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取当前设置的应用程序主题。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns><see cref="ThemeType.Unknow"/> if something goes wrong.</returns>
|
|||
|
|
public static ThemeType GetAppTheme()
|
|||
|
|
{
|
|||
|
|
if (_activeApplicationTheme == ThemeType.Unknow)
|
|||
|
|
{
|
|||
|
|
ResourceDictionaryManager appDictionaries = new();
|
|||
|
|
var themeDictionary = appDictionaries.LookupDictionary("themes");
|
|||
|
|
|
|||
|
|
if (themeDictionary == null)
|
|||
|
|
{
|
|||
|
|
return ThemeType.Unknow;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var themeUri = themeDictionary.Source.ToString();
|
|||
|
|
|
|||
|
|
if (themeUri.Contains("light", StringComparison.OrdinalIgnoreCase))
|
|||
|
|
{
|
|||
|
|
_activeApplicationTheme = ThemeType.Light;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (themeUri.Contains("dark", StringComparison.OrdinalIgnoreCase))
|
|||
|
|
{
|
|||
|
|
_activeApplicationTheme = ThemeType.Dark;
|
|||
|
|
}
|
|||
|
|
if (themeUri.Contains("hcwhite", StringComparison.OrdinalIgnoreCase))
|
|||
|
|
{
|
|||
|
|
_activeApplicationTheme = ThemeType.HcWhite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (themeUri.Contains("hcblack", StringComparison.OrdinalIgnoreCase))
|
|||
|
|
{
|
|||
|
|
_activeApplicationTheme = ThemeType.HcBlack;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return _activeApplicationTheme;
|
|||
|
|
}
|
|||
|
|
#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}", "WPFluent");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|