继承ContentControl来管理内容的加载
ContentControl
WPF的ContentControl是WPF控件的一种特殊形式,用于存储用户输入或从任何其他数据源读取的内容。内容控件只能包含一个子元素。这与包含多个子元素的布局控件(如Grid、WrapPanel和StackPanel控件)不同。
我们通过微软的资料就可以去看见它的定义和用途: 继承
ContentControl
派生
System.Activities.Presentation.View.ExpressionTextBox
System.Activities.Presentation.View.TypePresenter
System.Activities.Presentation.WorkflowElementDialog
System.Activities.Presentation.WorkflowItemPresenter
System.Activities.Presentation.WorkflowItemsPresenter
几乎所有可以使用Content的控件都是继承于它的,比如:
ButtonCheckBoxRadioButtonToggleButtonRepeatButton还有容器与窗口类:Window(WPF 中的顶级窗口)UserControl(用户自定义控件)ScrollViewer(滚动视图)GroupBox(带标题的边框容器)Frame(用于导航的框架) 或者说数据显示与修饰类:Label(注意:TextBlock不是ContentControl,但Label是。区别在于Label可以放图片,TextBlock只能放文字)ToolTip(鼠标悬停提示)StatusBarItem列表项:ListBoxItemComboBoxItemTabItemTreeViewItem(它比较特殊,既是ContentControl又是ItemsControl,因为它有 Header 也有子节点)
ContentControl可以包含任何类型的公共语言运行时对象 (,例如字符串或DateTime对象) 或UIElement对象 (,如 Rectangle 或 Panel) 。 这使你能够将丰富内容添加到 控件,例如 Button 和 CheckBox。
与他相对的就是条目控件ItemsControl,它则是可以容纳N个控件,我们可以控制子组件的加载周期,ItemsControl有一个核心机制叫做容器生成器,数据进来的时候,为每个数据包裹一个容器,例如ListBoxItems或者默认ContentPresenter,我们就可以监听这个容器的Loaded和Unloaded事件。
实现子组件加载的动画,比如说左侧滑入,我们可以修改ItemsControl的ItemContainerStyle来实现。
<ItemsControl ItemsSource="{Binding MyItems}">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Style.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
这个就不细讲了,我们可以在另外的笔记里面细讲。
主要内容
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace TheTestSolutionForWPF.Views;
public class ContentWrapper : ContentControl
{
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register(
nameof(IsActive), typeof(bool), typeof(ContentWrapper), new PropertyMetadata(true, OnIsActiveChanged));
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
public ContentWrapper()
{
this.Background = Brushes.Transparent;
var group = new TransformGroup();
group.Children.Add(new ScaleTransform(1, 1));
group.Children.Add(new TranslateTransform(0, 0));
this.RenderTransform = group;
this.RenderTransformOrigin = new Point(0.5, 0.5); // 从中心缩放
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ var control = (ContentWrapper)d;
bool newValue = (bool)e.NewValue;
if (newValue)
{ control.AnimateIn();
} else
{
control.AnimateOut();
} } private void AnimateIn()
{ this.Visibility = Visibility.Visible;
var springEase = new ElasticEase
{
Oscillations = 1,
Springiness = 5
};
var scaleAnim = new DoubleAnimation(0.8, 1.0, TimeSpan.FromSeconds(0.8))
{ EasingFunction = springEase
};
var opacityAnim = new DoubleAnimation(0, 1, TimeSpan.FromSeconds(0.3));
var transformGroup = this.RenderTransform as TransformGroup;
var scaleTransform = transformGroup.Children[0] as ScaleTransform;
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnim);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnim);
this.BeginAnimation(UIElement.OpacityProperty, opacityAnim);
} private void AnimateOut()
{ var smoothEase = new QuadraticEase { EasingMode = EasingMode.EaseOut };
var scaleAnim = new DoubleAnimation(1.0, 0.9, TimeSpan.FromSeconds(0.3))
{ EasingFunction = smoothEase
};
var opacityAnim = new DoubleAnimation(1, 0, TimeSpan.FromSeconds(0.2));
opacityAnim.Completed += (s, e) =>
{
if (!this.IsActive)
{
this.Visibility = Visibility.Collapsed;
} };
var transformGroup = this.RenderTransform as TransformGroup;
var scaleTransform = transformGroup.Children[0] as ScaleTransform;
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnim);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnim);
this.BeginAnimation(UIElement.OpacityProperty, opacityAnim);
}}
上面的代码是行为封装在自定义控制内部的模式也就是:Logic-less XAML, Logic-rich Control 是符合WPF/Avalonia的高级开发习惯的。
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register(
nameof(IsActive), typeof(bool), typeof(ContentWrapper), new PropertyMetadata(true, OnIsActiveChanged));
这里是我们注册依赖属性的地方,我们注册了一个叫做IsActive的属性,类型是Bool,属于一个叫做ContentWrapper的类,这个属性默认为True,最后面部分我们注册了一个回调函数,这个是属性的监听器,当IsActive从True变为了Flase的时候或者反之,他就会调用这个静态方法。
我们的AnimateIn和AnimateOut就是靠这个钩子来触发的。
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
这个地方是依赖属性的CLR包装器,GetValue因为返回的是object,我们必须进行强制转换使用bool。SetValue用于写入。
public ContentWrapper() {
this.Background = Brushes.Transparent;
var group = new TransformGroup();
group.Children.Add(new ScaleTransform(1, 1));
group.Children.Add(new TranslateTransform(0, 0));
this.RenderTransform = group;
this.RenderTransformOrigin = new Point(0.5, 0.5); // 从中心缩放
}
ContentControl 的 Background 默认是 null。在 WPF/Avalonia 中,null 意味着**“点击穿透”**(Hit Test Invisible)。也就是说,如果你不设置背景,鼠标点在控件空白处时,事件会直接穿透下去,仿佛这个控件不存在。
下面则是创建了一个组合动画,我们创建了一个缩放和变换动画,初始化值 (1, 1) 和 (0, 0):这是“无变化”状态,保证控件刚显示时是正常的 1:1 大小,位置不动。别忘记编写锚点,很多人会忘记,因为锚点的默认值是0,0 不写的话就会向着左上角收缩(如果你就想要这个效果就随意)。
终于来到了我们的回调函数
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (ContentWrapper)d;
bool newValue = (bool)e.NewValue;
if (newValue)
{
control.AnimateIn();
}
else
{
control.AnimateOut();
}}
它是一个静态的,因为WPF的依赖属性是注册在类层面上,而不是某个具体的对象,内存里只有一份IsActiveProperty的定义,因为函数是静态的,他不知道哪个控件变了,因此WPF会把出事的控件实例通过参数d传递进来。
我们通过了强制类型转换拿到了我们想要的control变量,才能使用AnimatIn和Out。参数e是非常有用的,他有两个属性,一个是OldValue一个是NewValuw以及Property(哪个属性变了),一般我们还会再利用OldValue实现一个防抖检查。
在WPF/Avalonia中,面对频繁开关的正确处理不是防抖,是截断,当新的指令来了的时候就会停止手头的动画,马上开始新的目标,你可能马上就理解了,就是使用取消令牌。
private CancellationTokenSource _debounceCts;
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (ContentWrapper)d;
// 1. 只要有新变化,先取消之前的计划
control._debounceCts?.Cancel();
control._debounceCts = new CancellationTokenSource();
var token = control._debounceCts.Token;
// 2. 延迟执行 (比如等 200ms)
Task.Delay(200, token).ContinueWith(t =>
{
if (t.IsCanceled) return; // 如果期间又变了,这次就作废
// 3. 只有等了 200ms 还没新变化,才真正执行 UI 操作
control.Dispatcher.Invoke(() =>
{
if ((bool)e.NewValue) control.AnimateIn();
else control.AnimateOut();
});
});
}