功能完善

This commit is contained in:
ShrlAlgo
2025-08-20 12:10:13 +08:00
parent d0cfc64450
commit fcd306b0f7
270 changed files with 11503 additions and 7193 deletions

View File

@@ -0,0 +1,25 @@
using System.Windows.Markup;
namespace NeumUI.Appearance;
/// <summary>
/// ControlsDictionary 类继承自 ResourceDictionary用于定义和管理控件相关的样式资源。
/// 该类在构造时自动加载指定路径下的样式资源文件,并将其设置为资源字典的源。
/// </summary>
/// <remarks>
/// 通过使用此字典,开发者可以方便地访问和应用应用程序中定义的一系列控件样式。这些样式通常包括颜色、字体以及其他视觉属性,
/// 有助于保持应用程序界面的一致性与美观。ControlsDictionary 在初始化时会指向一个预定义的样式文件(通过 ThemeManager.ThemesDictionaryPath 和 "Styles.xaml" 构建的 URI
/// 从而确保了所有引用该字典的控件都能够正确地展示出设计者所期望的外观。
/// </remarks>
[Localizability(LocalizationCategory.Ignore)]
[Ambient]
[UsableDuringInitialization(true)]
public class ControlsDictionary : ResourceDictionary
{
private static string DictionaryUri => $"{ThemeManager.ThemesDictionaryPath}Styles.xaml";
/// <summary>
/// 初始化一个新实例<see cref="ControlsDictionary"/>类。 控件字典的默认构造函数定义<see cref="ResourceDictionary.Source"/>
/// </summary>
public ControlsDictionary() { Source = new Uri(DictionaryUri, UriKind.Absolute); }
}

View File

@@ -0,0 +1,130 @@
using System.Collections.ObjectModel;
using System.Reflection;
using NeumUI.Extensions;
namespace NeumUI.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 (var 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 (var 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;
}
}
}
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;
}
}
}
// 未找到匹配的资源字典返回false
return false;
}
}

View File

@@ -0,0 +1,342 @@
using System.Reflection;
using System.Windows.Media.Animation;
using NeumUI.Extensions;
namespace NeumUI.Appearance;
/// <summary>
/// 允许通过交换包含动态资源的资源字典来管理应用程序主题。
/// </summary>
/// <example>
/// <code lang="csharp"> ThemeManager.SwitchThemeMode();</code>
/// </example>
public class ThemeManager
{
/// <summary>
/// 获取当前程序集的命名空间。
/// 该属性返回一个字符串,表示执行此代码的程序集的名称。此命名空间用于构建资源字典路径或其它需要引用程序集名称的地方。
/// </summary>
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>
public static ThemeMode GetAppThemeMode()
{
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);
}
/// <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);
}
/// <summary>
/// 更改当前应用程序明暗主题。
/// </summary>
public static void SwitchThemeMode()
{
GetAppThemeMode();
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
.WriteLine($"INFO | {typeof(ThemeManager)} application is {this.application}", ThemeManager.LibraryNamespace);
}
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
}

View File

@@ -0,0 +1,85 @@
using System.ComponentModel;
namespace NeumUI.Appearance;
/// <summary>
/// 明暗模式切换
/// </summary>
public enum ThemeMode
{
/// <summary>
/// 表示明亮模式,适用于偏好浅色背景的用户界面设置。
/// </summary>
Light,
/// <summary>
/// 表示暗模式,适用于偏好深色背景的用户界面设置。
/// </summary>
Dark
}
/// <summary>
/// 主题色切换
/// </summary>
public enum ThemePalette
{
/// <summary>
/// 薄暮红
/// </summary>
[Description("薄暮红")]
DustRed,
/// <summary>
/// 火山红
/// </summary>
[Description("火山红")]
Volcano,
/// <summary>
/// 日落橙
/// </summary>
[Description("日落橙")]
SunsetOrange,
/// <summary>
/// 金盏花
/// </summary>
[Description("金盏花")]
CalendulaGold,
/// <summary>
/// 日出黄
/// </summary>
[Description("日出黄")]
SunriseYellow,
/// <summary>
/// 青柠绿
/// </summary>
[Description("青柠绿")]
Lime,
/// <summary>
/// 极光绿
/// </summary>
[Description("极光绿")]
PolarGreen,
/// <summary>
/// 明青
/// </summary>
[Description("明青")]
Cyan,
/// <summary>
/// 拂晓蓝
/// </summary>
[Description("拂晓蓝")]
DaybreakBlue,
/// <summary>
/// 极客蓝
/// </summary>
[Description("极客蓝")]
GeekBlue,
/// <summary>
/// 酱紫
/// </summary>
[Description("酱紫")]
GoldenPurple,
/// <summary>
/// 法式洋红
/// </summary>
[Description("法式洋红")]
Magenta,
}

View File

@@ -0,0 +1,58 @@
using System.ComponentModel;
using System.Windows.Markup;
namespace NeumUI.Appearance;
/// <summary>
/// 提供字典实现,其中包含组件和其他设备使用的主题资源。
/// </summary>
[Localizability(LocalizationCategory.Ignore)]
[Ambient]
[UsableDuringInitialization(true)]
public class ThemesDictionary : ResourceDictionary
{
/// <summary>
/// 提供字典实现,其中包含组件和其他设备使用的主题资源。此字典能够根据当前应用程序的主题模式(如浅色或深色)自动加载相应的资源文件,并且在设计时会提供默认的静态主题以支持智能感知和可视化设计器。
/// </summary>
/// <remarks>
/// 该类继承自<see cref="ResourceDictionary"/>,并扩展了特定于主题管理的功能。它通过检查是否处于设计模式来决定加载设计时专用资源还是运行时实际应用的主题资源。此外,还提供了设置当前主题模式的方法,允许动态切换应用的主题。
/// </remarks>
public ThemesDictionary()
{
// 2. 在构造函数的最开始检查是否处于设计模式
if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
// 如果是在设计器中,直接加载一个默认的、静态的主题文件
// 这就是专门给设计器和智能感知“看”的资源
// 这样,智能感知就能找到所有资源了
Source = new Uri($"{ThemeManager.ThemesDictionaryPath}Light.xaml", UriKind.Absolute);
// 加载完设计时资源后,直接返回,不执行任何运行时的逻辑
return;
}
SetSourceBasedOnSelectedTheme(ThemeMode.Light);
}
private void SetSourceBasedOnSelectedTheme(ThemeMode? selectedApplicationTheme)
{
// 在运行时,我们不希望执行设计时逻辑,所以要再次检查
if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
return;
}
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); }
}