依赖属性和附加属性
依赖属性是扩展对象自身的属性功能,比如说是数据绑定、动画,属性是属于定义它的类的,最经典的场景是Button的background。依赖属性的主要作用是提供一种比普通的 .NET 属性更强大的内存管理和功能支持。
- 数据绑定: 只有依赖属性才能作为 Data Binding 的目标。
- 样式与模板: 支持通过 Style 或 ControlTemplate 进行统一修改。
- 属性值优先级: 它可以根据优先级(如动画 > 本地值 > 样式 > 默认值)自动计算最终结果。
- 资源节约: 如果属性没有被赋值,它不会占用实例内存,而是指向默认值。
示例: Button 的 Content 属性。按钮自己拥有这个属性,并负责显示它。
怎样自己定义一个依赖属性呢?C#属性的定义大家再熟悉不过了:
// 1. 使类型继承DependencyObject类
public class Person : DependencyObject
{
// 2. 声明一个静态只读的DependencyProperty 字段
public static readonly DependencyProperty nameProperty;
static Person()
{
// 3. 注册定义的依赖属性
nameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Person),
new PropertyMetadata("Learning Hard",OnValueChanged));
}
// 4. 属性包装器,通过它来读取和设置我们刚才注册的依赖属性
public string Name
{
get { return (string)GetValue(nameProperty); }
set { SetValue(nameProperty, value); }
}
private static void OnValueChanged(DependencyObject dpobj, DependencyPropertyChangedEventArgs e)
{
// 当只发生改变时回调的方法
}
}
依赖属性的优先级
WPF允许在多个地方设置依赖属性的值,则自然就涉及到依赖属性获取值的优先级问题。例如下面XMAL代码,我们在三个地方设置了按钮的背景颜色,那最终按钮会读取那个设置的值呢?是Green、Yellow还是Red?
<Window x:Class="DPSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="myButton" Background="Green" Width="100" Height="30">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Yellow"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
Click Me
</Button>
</Grid>
</Window>
上面按钮的背景颜色是Green。之所以背景色是Green,是因为WPF每访问一个依赖属性,它都会按照下面的顺序由高到底处理该值。具体优先级如下图所示:
![[Pasted image 20251213045459.png]]
按钮的本地值设置的是Green,自定义Style Trigger设置的为Red,自定义的Style Setter设置的为Yellow,由于这里的本地值的优先级最高,所以按钮的背景色或者的是Green值。如果此时把本地值Green去掉的话,此时按钮的背景颜色是Yellow而不是Red。这里尽管Style Trigger的优先级比Style Setter高,但是由于此时Style Trigger的IsMouseOver属性为false,即鼠标没有移到按钮上,一旦鼠标移到按钮上时,此时按钮的颜色就为Red。此时才会体现出Style Trigger的优先级比Style Setter优先级高。所以上图中优先级是比较理想情况下,很多时候还需要具体分析。
依赖属性的继承
依赖属性是可以被继承的,即父元素的相关设置会自动传递给所有的子元素。下面代码演示了依赖属性的继承。
<Window x:Class="Custom_DPInherited.DPInherited"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
FontSize="18"
Title="依赖属性的继承">
<StackPanel >
<Label Content="继承自Window的FontSize" />
<Label Content="显式设置FontSize"
TextElement.FontSize="36"/>
<StatusBar>Statusbar没有继承自Window的FontSize</StatusBar>
</StackPanel>
</Window>
![[Pasted image 20251213045731.png]]
在上面XAML代码中。Window.FontSize设置会影响所有内部子元素字体大小,这就是依赖属性的继承。如第一个Label没有定义FontSize,所以它继承了Window.FontSize值。但一旦子元素提供了显式设置,这种继承就会被打断,所以Window.FontSize值对于第二个Label不再起作用。
适用场景:
- 自定义控件的属性(如 Button.Content、TextBox.Text)。
- 需要支持数据绑定、动画、样式等 WPF 功能的属性。
附加属性(Attached Property)
用途:为其他控件(尤其是无法修改源码的控件)扩展功能(如 Grid、Canvas)中为子控件定义额外的属性。通过静态方法跨类附加属性,解耦功能与控件。
public static readonly DependencyProperty MyAttachedPropertyProperty =
DependencyProperty.RegisterAttached(
"MyAttachedProperty", // 属性名
typeof(int), // 属性类型
typeof(OwnerClass), // 定义该属性的类
new PropertyMetadata(0)); // 默认值
访问方式:
// Getter
public static int GetMyAttachedProperty(DependencyObject obj)
{
return (int)obj.GetValue(MyAttachedPropertyProperty);
}
// Setter
public static void SetMyAttachedProperty(DependencyObject obj, int value)
{
obj.SetValue(MyAttachedPropertyProperty, value);
}
- 适用场景:
- 布局控件的子元素属性(如
Grid.Row、Canvas.Left)。 - 扩展已有控件的功能(如
ToolTipService.ToolTip)。
- 布局控件的子元素属性(如
关键区别
![[Pasted image 20251213181131.png]]
时钟控制法
这个方法的原理核心可以总结为:“借鸡生蛋”(借用动画系统的计算能力,手动执行属性赋值)。
通常你写 <DoubleAnimation Storyboard.TargetProperty="Width" .../> 时,WPF 内部发生的事情是:
- 计算: 动画系统根据时间(0s -> 1s)和缓动函数(Easing),算出一个当前的数值(比如 100 -> 150)。
- 赋值: 动画系统直接把这个
double数值赋给目标属性。 但这种方法是对GridLength 没用,因为这个类型体是GridLength而不是double,这个是区别。 说回到时钟控制上,这个实际上是吧计算和赋值给分开,
var animation = new DoubleAnimation { To = 5, ... };
var clock = animation.CreateClock(); // 创建一个时钟
这里这个clock就是计时器,他知道当前的技术,也知道当前的进度,可以给我们计算后面的数值。
clock.CurrentTimeInvalidated += (s, e) =>
{
var currentVal = (double)clock.CurrentProgress * (To - fromValue) + fromValue;
if (currentVal < 0) currentVal = 0;
if (target is ColumnDefinition c)
c.Width = new GridLength(currentVal, GridUnitType.Star); // 强制设为 Star 单位
else if (target is RowDefinition r)
r.Height = new GridLength(currentVal, GridUnitType.Star);
};
这里他会拦截每一个帧数,把进度转换为double,然后计算出当前的数值,然后赋值。它的优势就是分开去计算。
传入动画达到复用率
public DoubleAnimation Animation { get; set; }
protected override void Invoke(object parameter)
{
var target = TargetObject as UIElement ?? AssociatedObject as UIElement;
if (target == null || string.IsNullOrEmpty(PropertyName)) return;
DoubleAnimation animToRun;
if (this.Animation != null)
{ animToRun = this.Animation.Clone();
} else
{
animToRun = new DoubleAnimation
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut }
}; } if (!double.IsNaN(this.To))
{ animToRun.To = this.To;
} // 当 Action 设置了 Duration 时才覆盖
if (this.Duration != default(Duration))
{ animToRun.Duration = this.Duration;
} target.BeginAnimation(
DependencyPropertyDescriptor.FromName(PropertyName, target.GetType(), target.GetType()).DependencyProperty,
Animation);
}
我们定义一个叫做Animation的属性,用来接收,如果没有传入那就使用提前设定好的默认动画。
if (!double.IsNaN(this.To)) : 如果开发者在外层Action上手动填了To的值,在 C# 和 WPF 里,double 类型的默认值通常是 0。
if (this.Duration != default(Duration)):Duration 是一个结构体(Struct),它不能为空(null),它的默认状态叫 Automatic。如果用户在外层标签写了 Duration="0:0:1",那么它就不等于默认值,我们就用这个时长强制覆盖内部动画的时长。