Themes
Colors
Thems负责的是主体系统,我们点开Basic文件夹里,可以看到的是Colors、ColorsDark、ColorsViolet。
![[Pasted image 20260402170708.png]]
这里看名称还是比较可以清晰明了的,基本是用Light/Dark这样的开头来阐述,还有一个文件叫做ColorsDark,里面保存的是黑暗主题的颜色代码,这两个对比的颜色就可以任意切换颜色主题。
而在Avalonia版本中,HandyControl使用了更现代的主题字典方式,在单个Colors.axaml文件中通过ResourceDictionary.ThemeDictionaries来管理不同主题的颜色 Colors.axaml:4-96 :
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<!-- 明亮主题颜色 -->
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<!-- 黑暗主题颜色 -->
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
定义的颜色资源会在Theme.xaml中被映射为各种画刷,供控件使用 Theme.xaml:838-888 。例如:
PrimaryColor→PrimaryBrush(渐变画刷)LightPrimaryColor→LightPrimaryBrush(纯色画刷)DarkPrimaryColor→DarkPrimaryBrush(纯色画刷)
先不聊Theme,因为还有其他重要的文件需要阅读,以及Theme这个文件实在过于庞大,很多的任务都在这里面处理。除了颜色画刷以外,theme还处理了大量的矢量图形资源,用于图标和装饰元素 Theme.xaml:91-94
<Geometry o:Freeze="True" x:Key="AllGeometry">M 721.005 638.949...</Geometry>
<Geometry o:Freeze="True" x:Key="DragVerticalGeometry">M2,12 C3.1045694,12...</Geometry>
<Geometry o:Freeze="True" x:Key="DropperGeometry">M798.165333 97.834667...</Geometry>
还有各种控件的基础样式,通用尺寸值 Theme.xaml:100-105:
<system:Double x:Key="DefaultControlHeight">28</system:Double>
<system:Double x:Key="SmallControlHeight">20</system:Double>
<Thickness x:Key="DefaultControlPadding">10,5</Thickness>
<CornerRadius x:Key="DefaultCornerRadius">4</CornerRadius>
动画行为、控件模板、阴影效果、基础控件样式、特殊画刷和模板、路径样式等等,十分的杂乱,在Avalonia版本里,这些都会被拆分为不同的文件中管理,如Geometries.axaml、Fonts.axaml、Effects.axaml等 Theme.axaml:8-16 ,使主题系统更加模块化(真的及其恶心)。
Behaviors
![[Pasted image 20260402172859.png]]
这个xaml文件封装了6个流体移动行为,用于实现平滑的动画效果。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:interactivity="clr-namespace:HandyControl.Interactivity">
<interactivity:FluidMoveBehavior x:Key="BehaviorXY200" x:Shared="False" AppliesTo="Children" Duration="0:0:.2">
<interactivity:FluidMoveBehavior.EaseX>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseX>
<interactivity:FluidMoveBehavior.EaseY>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseY>
</interactivity:FluidMoveBehavior>
<interactivity:FluidMoveBehavior x:Key="BehaviorX200" x:Shared="False" AppliesTo="Children" Duration="0:0:.2">
<interactivity:FluidMoveBehavior.EaseX>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseX>
</interactivity:FluidMoveBehavior>
<interactivity:FluidMoveBehavior x:Key="BehaviorY200" x:Shared="False" AppliesTo="Children" Duration="0:0:.2">
<interactivity:FluidMoveBehavior.EaseY>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseY>
</interactivity:FluidMoveBehavior>
<interactivity:FluidMoveBehavior x:Key="BehaviorXY400" x:Shared="False" AppliesTo="Children" Duration="0:0:.4">
<interactivity:FluidMoveBehavior.EaseX>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseX>
<interactivity:FluidMoveBehavior.EaseY>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseY>
</interactivity:FluidMoveBehavior>
<interactivity:FluidMoveBehavior x:Key="BehaviorX400" x:Shared="False" AppliesTo="Children" Duration="0:0:.4">
<interactivity:FluidMoveBehavior.EaseX>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseX>
</interactivity:FluidMoveBehavior>
<interactivity:FluidMoveBehavior x:Key="BehaviorY400" x:Shared="False" AppliesTo="Children" Duration="0:0:.4">
<interactivity:FluidMoveBehavior.EaseY>
<PowerEase/>
</interactivity:FluidMoveBehavior.EaseY>
</interactivity:FluidMoveBehavior>
</ResourceDictionary>
有双轴移动效果(X、Y两个一起) 持续200ms 400ms的,也有各个单轴的效果,这些行为都是配置了共同属性x:Shared=“False”可以确保每次使用的时候都会创建新的实例。
AppliesTo="Children" - 仅应用于子元素, PowerEase - 使用PowerEase缓动函数。
重点不是动画,而是搞懂为什么要设定共同属性为什么这么设置,一个是通过预定义行为的资源,避免在每个控件中重复配置相同的动画参数,所有行为共享统一的配置模式,修改时只需更新一处。
然后是性能方面,每一次使用的是创建实例,可以避免行为实例在多个控件之间共享时可能产生的状态冲突 Theme.xaml:106-113 。
PowerEase是一个WPF/.NET Framework的一个缓动函数类,属于System.Windows.Media.Animation命名空间,它继承自EasingFunctionBase,用于创建具有加速和减速效果的动画。
我们接下来看看行为依赖的interactivity。
Interactivity
![[Pasted image 20260402205532.png]]
它对应的是HandyControl.Interactivity命名空间,包含以下核心组件:
Interaction附加属性容器,用于提供Interaction.Behaviors附加属性,用于将行为附加到UI元素上
ControlCommands命令定义,包含控件相关的命令定义,如TabControl中使用的关闭命令等。
我们先看看Command文件夹:具体是什么? ControlCommand.cs是命令的总入口,统一定义了很多的RoutedCommand,以及几个自定义的ICommand实例,然后其他的:
CloseWindowCommand.cs:关闭当前元素所在窗口OpenLinkCommand.cs:打开链接ShutdownAppCommand.cs:关闭应用PushMainWindow2TopCommand.cs:把主窗口显示并置顶到前台StartScreenshotCommand.cs:启动截图ScreenshotCommand.cs:内容与StartScreenshotCommand基本重复(看起来像历史遗留/重复文件)
在架构中的作用就是动作语义从控件里抽离出来,避免每个控件自己写一套点击逻辑,让XAML里面可以直接写命令来绑定,比如说Command="interactivity:ControlCommands.Prev"
结合 Interactivity(行为、触发器)时,能做“声明式交互”。你可以把它理解为:控件行为协议层。
写到这里,是不是显得十分的分散,接下来用一整条回路来解释它的作用。
命令 -> 到组件里
Theme.xaml 的 Pagination 模板,把按钮命令写成 ControlCommands.Prev/Next/Jump:
<ControlTemplate TargetType="hc:Pagination">
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Button x:Name="PART_ButtonLeft" Command="interactivity:ControlCommands.Prev" />
...
<Button x:Name="PART_ButtonRight" Command="interactivity:ControlCommands.Next" />
...
<Button ... Command="interactivity:ControlCommands.Jump" />
</StackPanel>
</ControlTemplate>
ControlCommands 里把这些命令定义成静态 RoutedCommand:
public static RoutedCommand Prev { get; } = new(nameof(Prev), typeof(ControlCommands));
public static RoutedCommand Next { get; } = new(nameof(Next), typeof(ControlCommands));
public static RoutedCommand Jump { get; } = new(nameof(Jump), typeof(ControlCommands));
控件构造时注册 CommandBinding,Pagination 在构造函数里“接住”这些命令:
public Pagination()
{
CommandBindings.Add(new CommandBinding(ControlCommands.Prev, ButtonPrev_OnClick));
CommandBindings.Add(new CommandBinding(ControlCommands.Next, ButtonNext_OnClick));
CommandBindings.Add(new CommandBinding(ControlCommands.Selected, ToggleButton_OnChecked));
CommandBindings.Add(new CommandBinding(ControlCommands.Jump, (s, e) => PageIndex = (int)_jumpNumericUpDown.Value));
}
最终执行业务逻辑层面上,比如 Prev/Next 最后就是改页码:
private void ButtonPrev_OnClick(object sender, RoutedEventArgs e) => PageIndex--;
private void ButtonNext_OnClick(object sender, RoutedEventArgs e) => PageIndex++;
事件 -> 命令(Interactivity 桥接)
Pagination 中间那组页码 RadioButton 的选中,不是直接 Command=,而是通过触发器桥接:
<interactivity:Interaction.Triggers>
<interactivity:RoutedEventTrigger RoutedEvent="RadioButton.Checked">
<interactivity:EventToCommand Command="interactivity:ControlCommands.Selected" PassEventArgsToCommand="True" />
</interactivity:RoutedEventTrigger>
</interactivity:Interaction.Triggers>
它的机制就是RoutedEventTrigger监听到路由事件之后,会调用EventToCommand.Invoke,当EventToCommand取到Command的时候会执行CanExecute/Execute来实现关键的实现点。
protected override void Invoke(object parameter)
{
...
if (command == null || !command.CanExecute(parameter1))
return;
command.Execute(parameter1);
}
自定义Icommand
除了 RoutedCommand,ControlCommands 也暴露了直接可执行的 ICommand 实例:
public static CloseWindowCommand CloseWindow { get; } = new();
Demo 里直接用:
<Button Command="hc:ControlCommands.CloseWindow" CommandParameter="{Binding RelativeSource={RelativeSource Self}}" .../>
命令实现就是:拿 CommandParameter 对应元素,找到所在窗口并关闭:
public void Execute(object parameter)
{
if (parameter is DependencyObject dependencyObject)
{
if (Window.GetWindow(dependencyObject) is { } window)
{
window.Close();
}
}
}
总结来说,ControlCommand会统一动作名册,Theme.xaml会把命令挂到可视元素,控件类CommandBindings会把命令映射到控件内部的逻辑。Interactivity会把事件桥接成命令,减少code-behind。
除开command文件,就是事件转命令的实现EventToCommand:
EventToCommand
为什么需要这个文件? 因为很多WPF很多事件不能直接优雅的走命令,但是组件库又想要模板层没有code-behind,实现交互统一命令化,EventToCommand 就是为这个缺口做的桥接器。
就比如说你有一个事件MouseMove,但是你希望执行ICommand,你想要写纯XAML来交互,不用写页面代码后置,还可以把控件启用状态跟CanExecue联动。Button 这种有 Command 属性的控件可以直接绑命令,但很多交互是“事件型”的(尤其在 Style/ControlTemplate、触发器场景),不总能直接命令绑定
就比如说这里:
<interactivity:Interaction.Triggers>
<interactivity:RoutedEventTrigger RoutedEvent="RadioButton.Checked">
<interactivity:EventToCommand Command="interactivity:ControlCommands.Selected" PassEventArgsToCommand="True" />
</interactivity:RoutedEventTrigger>
</interactivity:Interaction.Triggers>
这里会在容器上挂载触发器,来监听冒泡上来的RadioButton.Checked事件,然后执行EventToCommand,EventToCommand会调用ControlCommands.Selected.Execute(...), PassEventArgsToCommand="True":把这次事件参数(RoutedEventArgs)作为命令参数传入(如果没显式 CommandParameter)。
![[Pasted image 20260402213939.png]]
有了它的帮助,我们可以把事件触发转换为命令执行,让你在 XAML 里不用写 Click 代码后置,而是通过触发器把事件映射到 ICommand。这个文件做了四件事:
- 接收命令与参数:
Command、CommandParameter - 支持把事件参数传给命令:
PassEventArgsToCommand、EventArgsConverter - 执行前做可执行判断:
CanExecute不通过就不执行 - 可选联动控件启用状态:
MustToggleIsEnabled时自动根据CanExecute设置控件IsEnabled
这里TriggerAction触发器接受的类型是DependencyObject,他可以提供数据绑定、样式/模板的Setter赋值。以及动画和属性值继承,还能实现默认值、回调、校验、coerce等机制(依赖属性被设置的时候,WPF在真正生效前,给你一次强制修正值的机会),WPF中绝大部分UI对象都在这条继承链上。
你可以打开DependencyObject来看看究竟有哪些: ![[Pasted image 20260402214930.png]]
这里的## [NameScopeProperty("NameScope", typeof(NameScope))]意思就是在xaml设计器中可以得到NameScope属性,来关系x:Name命名作用域, NameScope 的作用是:在某个作用域内,名字唯一,并支持 FindName、Storyboard 目标解析等。
回到刚才的话题,它注册了几个依赖属性,- 给 EventToCommand 提供一个可绑定的参数入口(XAML/Binding 可赋值), 这个参数会在触发时传给命令的 Execute(parameter) / CanExecute(parameter)
MouseDragElementBehaviorEx
这个文件是反编译微软的System.Windows.Interactivity程序集得到的,并对其做了些扩展。他们做了LockX和LockY两个轴向锁属性,在位移应用处加入了轴向判断,代码里也写了“在该处对微软的类进行了修改”):
MatrixTransform分支里,只有!LockX才改OffsetX,只有!LockY才改OffsetYTranslateTransform分支同理,只在未锁定轴上累加位移
也就是把原来“自由拖拽(X/Y都动)”扩展成“可限制只横向拖或只纵向拖”。这样就得到了一个可以附加到FrameworkElement 的拖拽行为(Behavior),核心能力:
- 鼠标按下开始拖拽(捕获鼠标)
- 鼠标移动实时更新元素位置(通过
RenderTransform平移) - 鼠标抬起/失去捕获结束拖拽
- 可选限制在父容器边界内(
ConstrainToParentBounds) - 通过依赖属性暴露当前位置(
X、Y) - 暴露拖拽生命周期事件:
DragBegun/Dragging/DragFinished
这是一个“可配置的 UI 拖拽引擎”:
在微软原版 MouseDragElementBehavior 基础上,HandyControl 增强了锁轴拖动(LockX/LockY),更适合做窗口条、滑块、面板等只允许单轴移动的交互。
behavior
这里使用或者说继承的behavior其实是作者们改良过的:![[Pasted image 20260402224005.png]]
这里的AssociatedObject指的是Behavior当前附加到的哪个对象的实例,当你在 XAML 里给某个元素加 Behavior 时(比如给 Grid、Button、FrameworkElement):
Attach(dependencyObject)被调用_associatedObject = dependencyObject- 之后
OnAttached()里你就可以通过AssociatedObject操作该元素(订阅事件、改属性等)
我们熟知的DragBehavior就是这样生效的!!
那么这里为什么不直接写Behavior<>里面Dep..Object来约束呢,为什么非要where来泛型约束。如果真的像前面这样来写的话,等于把类型信息丢了,子类里你还是要反复强转。Behavior<T> 的意义是把“宿主类型约束”前置到编译期。
这么说你就懂了, DependencyObject 太宽泛:Window、Button、Transform、Brush 都是它体系里的类型 你的行为通常只适用于某一类宿主(比如 FrameworkElement)
- 用泛型
T可以:- 编译期拿到强类型
AssociatedObject - Attach 时自动做类型约束检查
- 减少运行时 cast/空判断和错误
- 编译期拿到强类型
RoutedEventTrigger
这个文件的职责很清晰,监听一个指定的 RoutedEvent,一旦触发就把事件转发给触发器动作链(比如 EventToCommand)。![[Pasted image 20260402225354.png]]
和我们刚才看到的
<interactivity:Interaction.Triggers>
<interactivity:RoutedEventTrigger RoutedEvent="RadioButton.Checked">
<interactivity:EventToCommand
Command="interactivity:ControlCommands.Selected"
PassEventArgsToCommand="True" />
</interactivity:RoutedEventTrigger>
</interactivity:Interaction.Triggers>
这段代码完全相关
Brushes文件
这个是颜色到画刷的中间层,我们之前见到的Color颜色不能直接使用,我们需要封装为可以给控件使用的Brush资源:
![[Pasted image 20260402231018.png]]
Conveters 转换器
![[Pasted image 20260402231347.png]]
它是“全局转换器注册表”
把 HandyControl 里常用的 IValueConverter / IMultiValueConverter 统一注册成可复用资源,供所有样式模板直接 StaticResource 引用。
- 布尔和可见性:
Boolean2VisibilityConverter、Boolean2VisibilityReConverter - 反转/组合逻辑:
Boolean2BooleanReConverter、BooleanArr2BooleanConverter - 布局/图形:
CornerRadiusSplitConverter、ThicknessSplitConverter、BorderClipConverter - 文本/数值:
Int2StringConverter、Number2PercentageConverter - 控件特定:
TreeViewItemMarginConverter、DataGridSelectAllButtonVisibilityConverter
为什么单独放一个文件
- 主题模板大量依赖 converter,集中定义可避免重复创建和重复声明
- 统一 key,样式里直接用,降低模板复杂度
- 作为主题基础资源的一部分,随主题一起合并加载
Effect文件
这个用于主题里的阴影效果资源表
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options">
<Color x:Key="EffectShadowColor">#88000000</Color>
<DropShadowEffect x:Key="EffectShadow1" BlurRadius="5" ShadowDepth="1" Direction="270" Color="{StaticResource EffectShadowColor}" Opacity=".2" RenderingBias="Performance" o:Freeze="True" />
<DropShadowEffect x:Key="EffectShadow2" BlurRadius="8" ShadowDepth="1.5" Direction="270" Color="{StaticResource EffectShadowColor}" Opacity=".2" RenderingBias="Performance" o:Freeze="True" />
<DropShadowEffect x:Key="EffectShadow3" BlurRadius="14" ShadowDepth="4.5" Direction="270" Color="{StaticResource EffectShadowColor}" Opacity=".2" RenderingBias="Performance" o:Freeze="True" />
<DropShadowEffect x:Key="EffectShadow4" BlurRadius="25" ShadowDepth="8" Direction="270" Color="{StaticResource EffectShadowColor}" Opacity=".2" RenderingBias="Performance" o:Freeze="True" />
<DropShadowEffect x:Key="EffectShadow5" BlurRadius="35" ShadowDepth="13" Direction="270" Color="{StaticResource EffectShadowColor}" Opacity=".2" RenderingBias="Performance" o:Freeze="True" />
</ResourceDictionary>
这里共享了一个半透明黑色:EffectShadowColor。 五档 DropShadowEffect:EffectShadow1~EffectShadow5,模糊半径和阴影深度递增,用于不同强度的卡片/浮层阴影
- 控件模板里用
Effect="{StaticResource EffectShadow3}"这类写法,统一视觉、避免每个控件手写一遍DropShadowEffect o:Freeze="True":冻结为不可变对象,便于性能与共享
和 Brushes.xaml、Converters.xaml 一样,属于基础主题资源,被合并进主主题后全局复用。
Fonts文件
这个就和名字一样,定义了各种文字的大小。![[Pasted image 20260403160604.png]]
在 Theme.xaml 顶部已经能看到类似资源被使用(比如 LargeFontSize、HeadFontSize、TextFontSize 这种 key),这些就是供控件样式统一引用的。
Geometry
这个文件定义了可复用的矢量几何资源(Geometry)。在主题体系里它通常负责这类内容:
- 各种图标路径(关闭、箭头、搜索、旋转、保存等)
- 复杂矢量形状(控件内部装饰用)
- 统一 key(如
CloseGeometry、LeftGeometry),供样式和模板复用
单独放在这个文件里的原因是为了避免每个模块里重复编写长Path Data,并且图标视觉统一就可以实现低维护成本, 配合 Path Data="{StaticResource XxxGeometry}" 使用很干净。
Theme.xaml 里已经能看到它的使用方式,比如按钮模板里常见:
hc:IconElement.Geometry="{StaticResource LeftGeometry}"...="{StaticResource CloseGeometry}"
![[Pasted image 20260403161431.png]]
Path文件夹
它的任务就是把刚才的几何层封装成可以直接使用的Path 样式, Geometries.xaml:只提供 Geometry 数据(图标“形状原料”)。
![[Pasted image 20260403161523.png]]
Size文件
它的作用是集中定义尺寸相关的主题常量资源,给全局的样式和控件模板统一使用。在 HandyControl 这种主题体系里,它一般会包含类似这些 key:
- 控件高度(默认/小号)
- 圆角半径(默认/胶囊)
- 边距、间距、内边距
- 图标尺寸、线宽等 ![[Pasted image 20260403161633.png]]
Base文件夹
Themes/Styles/Base 里放的是各控件的“基础样式模板层”,可以理解成“样式骨架”。控件默认使用的是ControlTemplate,通用Setter,边框、内边距、状态触发器。以及设置了供上层使用的x:key。 这个也是组件设计的基本功,先定义可重复使用的基础外观和行为,外层样式再去轻量定制。![[Pasted image 20260403165637.png]]
就像上面的,我们看到的这个基础的样式,它最终会被作为下拉项样式来使用。具体关系是: AutoCompleteTextBoxItemBaseStyle 的 TargetType 是 ComboBoxItem 。
在 AutoCompleteTextBoxBaseStyle 里通过ItemContainerStyle = {StaticResource AutoCompleteTextBoxItemBaseStyle}
应用给 hc:AutoCompleteTextBox 的每一项
我们可以直接看一下AutoCompleteTextBox在哪里,![[Pasted image 20260403174939.png]]
这里就是定义了这个组件的地方。
我们主要学习的就是别人是怎么自定义组件的,因此这段代码我们重点来研究,之后我们也会这样来完成。
[TemplatePart(Name = SearchTextBox, Type = typeof(System.Windows.Controls.TextBox))]
这个特性用于声明“这个自定义控件的模板里,预期应该有哪个命名部件(PART_...)以及它的类型”。
里应包含了名为SearchTextBox,通常是某个PART_xxx常量值的元素,该元素类型是TextBox。这样的模式便于开发者一个模板提醒(提醒这个模板里应该有一个名为SearchTextBox的部件,类型是TextBox,但是真正的控件实例必须在ControlTemplate里面自己写出来,比如说x:Name="PART_SearchTextBox",刚才我们看到了)。 便于控件在 OnApplyTemplate() 里用 GetTemplateChild(...) 取到部件并绑定逻辑
- 这个特性本身不强制运行时校验
- 真正是否存在、能不能用,要看控件代码在
OnApplyTemplate里怎么处理(判空/抛错等)
资源加载
在WPF中,资源字典可能会重复加载导致的内存膨胀。如果在大型项目中,不使用SharedResourceDictionary,每当你引用一次通用的 XAML 资源(比如 Colors.xaml),系统就会在内存中完整地重新创建一套这些对象的副本。
在标准的 ResourceDictionary 中,当你这样写:
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
</ResourceDictionary.MergedDictionaries>
如果你有100个窗口或者是用户控件都会引用这个文件的话,WPF就会默认解析和加载100次,十分耗费CPU,因此我们可以使用一个静态的缓存池接管加载过程。
public static Dictionary<Uri, ResourceDictionary> SharedDictionaries = new();
这是一个全局共享的字典。它以 Uri(资源路径)作为 Key,已经解析好的 ResourceDictionary 对象作为 Value。
public class SharedResourceDictionary : ResourceDictionary
{
public static Dictionary<Uri, ResourceDictionary> SharedDictionaries = new();
private Uri _sourceUri;
public new Uri Source
{
get => DesignerHelper.IsInDesignMode ? base.Source : _sourceUri;
set
{
if (value == null) return;
if (DesignerHelper.IsInDesignMode)
{
base.Source = value;
return;
}
_sourceUri = value;
if (!SharedDictionaries.ContainsKey(value))
{
base.Source = value;
SharedDictionaries.Add(value, this);
}
else
{
MergedDictionaries.Add(SharedDictionaries[value]);
}
}
}
}
他通过new关键字隐藏了基类的Source属性,可以重新定义赋值逻辑,当设置 Source 时,先去 SharedDictionaries 看看这个路径的资源是否已经加载过了。如果没加载过,调用 base.Source = value 进行解析,并把结果存入静态字典。如果已经加载过,不再重新解析,而是直接把缓存中的实例添加到当前的 MergedDictionaries 中。
主题管理器
将原本静态的 ResourceDictionary(资源字典)扩展成了一个功能强大的主题管理器就可以实现三个自动化,皮肤切换、
public class Theme : ResourceDictionary
{
public Theme()
{
if (DesignerHelper.IsInDesignMode)
{
MergedDictionaries.Add(ResourceHelper.GetSkin(SkinType.Default));
MergedDictionaries.Add(ResourceHelper.GetTheme());
}
else
{
InitResource();
}
}
#region Skin
private SkinType _manualSkinType;
private SkinType _skin;
public virtual SkinType Skin
{
get => _skin;
set
{
if (_skin == value) return;
_skin = value;
UpdateSkin();
}
}
public static readonly DependencyProperty SkinProperty = DependencyProperty.RegisterAttached(
"Skin", typeof(SkinType), typeof(Theme), new PropertyMetadata(default(SkinType), OnSkinChanged));
private static void OnSkinChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not FrameworkElement element)
{
return;
}
var skin = (SkinType) e.NewValue;
var themes = new List<Theme>();
GetAllThemes(element.Resources, ref themes);
if (themes.Count > 0)
{
foreach (var theme in themes)
{
theme.Skin = skin;
}
}
else
{
element.Resources.MergedDictionaries.Add(new Theme
{
Skin = skin
});
}
}
private static void GetAllThemes(ResourceDictionary resourceDictionary, ref List<Theme> themes)
{
if (resourceDictionary is Theme theme)
{
themes.Add(theme);
}
// we must consider it's MergedDictionaries
foreach (var dictionaryMergedDictionary in resourceDictionary.MergedDictionaries)
{
GetAllThemes(dictionaryMergedDictionary, ref themes);
}
}
public static void SetSkin(DependencyObject element, SkinType value)
=> element.SetValue(SkinProperty, value);
public static SkinType GetSkin(DependencyObject element)
=> (SkinType) element.GetValue(SkinProperty);
#endregion
#region SyncWithSystem
private bool _syncWithSystem;
public bool SyncWithSystem
{
get => _syncWithSystem;
set
{
_syncWithSystem = value;
if (value)
{
_manualSkinType = _skin;
SyncWithSystemTheme();
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
}
else
{
SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;
_skin = _manualSkinType;
UpdateSkin();
}
}
}
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
if (e.Category == UserPreferenceCategory.General)
{
SyncWithSystemTheme();
}
}
private void SyncWithSystemTheme()
{
_skin = SystemHelper.DetermineIfInLightThemeMode() ? SkinType.Default : SkinType.Dark;
UpdateSkin();
}
#endregion
#region AccentColor
private Color? _accentColor;
public Color? AccentColor
{
get => _accentColor;
set
{
_accentColor = value;
if (value == null)
{
_precSkin = null;
}
UpdateSkin();
}
}
#endregion
#region Source
private Uri _source;
public new Uri Source
{
get => DesignerHelper.IsInDesignMode ? null : _source;
set => _source = value;
}
#endregion
public string Name { get; set; }
private SkinType _prevSkinType;
private ResourceDictionary _precSkin;
public virtual ResourceDictionary GetSkin(SkinType skinType)
{
if (_precSkin == null || _prevSkinType != skinType)
{
_precSkin = ResourceHelper.GetSkin(skinType);
_prevSkinType = skinType;
}
if (!SyncWithSystem)
{
if (AccentColor != null)
{
UpdateAccentColor(AccentColor.Value);
}
}
else
{
InteropMethods.DwmGetColorizationColor(out var color, out _);
UpdateAccentColor(ColorHelper.ToColor(color));
}
return _precSkin;
}
public static Theme GetTheme(string name, ResourceDictionary resourceDictionary)
{
if (string.IsNullOrEmpty(name) || resourceDictionary == null)
{
return null;
}
return resourceDictionary.MergedDictionaries.OfType<Theme>().FirstOrDefault(item => Equals(item.Name, name));
}
public virtual ResourceDictionary GetTheme() => ResourceHelper.GetTheme();
private void InitResource()
{
if (DesignerHelper.IsInDesignMode)
{
return;
}
MergedDictionaries.Clear();
MergedDictionaries.Add(GetSkin(Skin));
MergedDictionaries.Add(GetTheme());
}
private void UpdateAccentColor(Color color)
{
_precSkin[ResourceToken.PrimaryColor] = color;
_precSkin[ResourceToken.DarkPrimaryColor] = color;
_precSkin[ResourceToken.TitleColor] = color;
_precSkin[ResourceToken.SecondaryTitleColor] = color;
}
private void UpdateSkin() => MergedDictionaries[0] = GetSkin(Skin);
}
public class StandaloneTheme : Theme
{
public override ResourceDictionary GetTheme() => ResourceHelper.GetStandaloneTheme();
}
这里和上一个文件不同的地方在于,这里主要是主题的相关策略,就比如说切换和控制之类的。
运行时汇总
![[Pasted image 20260406174534.png]]
Theme.xaml是一份极为庞大的文件,Themes/Styles 是“源码拆分层”,Theme.xaml 是“运行时汇总产物层”。
这是为了同时兼具方便人维护和方便运行时加载的功能,运行时一般只加载:
pack://application:,,,/HandyControl;component/Themes/Theme.xaml这点在ResourceHelper.GetTheme()里就能看到。Theme_GE45.txt/Theme_40.txt明确列出了要合并的文件清单(包含Basic/*、Styles/*、Styles/Base/*)Themes/XamlCombine.exe存在,说明有“把拆分样式合并成 Theme.xaml”的构建流程Theme.xaml顶部也写了 generated 注释(自动生成)