【问题标题】:Property Value Animation triggered by a custom DependencyProperty in UserControl?由 UserControl 中的自定义 DependencyProperty 触发的属性值动画?
【发布时间】:2015-03-26 21:29:54
【问题描述】:

问题前奏:

如何为RotateTransformAngle 属性设置动画 一个 UIElement A 当自定义 DependencyProperty 的值 当我点击一个 boolean 类型的 UIElement B,都在 UserControl 内? 仅在 XAML 中(或大部分)?如果可能的话:)

我编写了以下所有内容,以提供所有我的问题所需的详细信息。您可以随时停止从上到下阅读;甚至直接跳转到帖子第一季度内的实际问题


上下文:

问题是关于动画触发器自定义属性绑定,它们都在一个UserControl中。到目前为止没有涉及任何窗口。

首先,假设我创建了一个 UserControl,它的主 Grid 包含另外两个 Grids >。最简单的模式:

<!-- MyControl.xaml -->
<UserControl ...blahblahblah>
    <Grid>
        <Grid x:Name="TiltingGrid">
            <!-- This Grid contains UIElements that I want to tilt alltogether -->
        </Grid>
        <Grid>
            <Ellipse x:Name="TiltingTrigger" ...blahblahblah>
                <!-- This Ellipse is my "click-able" area -->
            </Ellipse>
        </Grid>
    </Grid>
</UserControl>

然后,在 Code Behind 中,我有一个名为 IsTiltingDependencyProperty

// MyControl.xaml.cs
public bool IsTilting
{
    // Default value is : false
    get { return (bool)this.GetValue(IsTiltingProperty); }
    set { this.SetValue(IsTiltingProperty, value); }
}

private static readonly DependencyProperty IsTiltingProperty =
    DependencyProperty.Register(
        "IsTilting",
        typeof(bool),
        typeof(MyControl),
        new FrameworkPropertyMetadata(
            false,
            new PropertyChangedCallback(OnIsTiltingPropertyChanged)));

private static void OnIsTiltingPropertyChanged(...) { ... }
    // .. is a classic Callback which calls
    // private void OnIsTiltingChanged((bool)e.NewValue)
    // and/or
    // protected virtual void OnIsTiltingChanged(e) ...

然后,我在 XAML 中为名为 TiltingGrid 的网格定义了一些属性:

<Grid x:Name="TiltingGrid"
    RenderTransformOrigin="0.3, 0.5">

    <Grid.RenderTransform>
        <RotateTransform 
            x:Name="TiltRotate" Angle="0.0" />
            <!-- Angle is the Property I want to animate... -->
    </Grid.RenderTransform>

    <!-- This Grid contains UIElements -->
    <Path ... />
    <Path ... />
    <Ellipse ... />
</Grid>

我想在单击此 UserControl 内的特定区域时触发倾斜:第二个网格中的椭圆:

<Grid>
    <Ellipse x:Name="TiltingTrigger"
        ... Fill and Stroke goes here ...
        MouseLeftButtonDown="TryTilt_MouseLeftButtonDown"
        MouseLeftButtonUp="TryTilt_MouseLeftButtonUp">
    </Ellipse>
</Grid>

如果我没记错的话,Ellipse 没有点击事件,所以我必须为MouseLeftButtonDownMouseLeftButtonUp 创建两个事件处理程序。我必须这样做才能做到:

  • 让 Ellipse 在 MouseLeftButtonDown 时捕获鼠标,并将私有字段设置为 true
  • 在MouseLeftButtonUp时测试鼠标点是否在椭圆内,将私有字段的值设置为false,然后释放鼠标。
  • 如果发生看起来像“点击”的事情,则反转 DependencyProperty IsTilting 的值(真/假)(如果我能够解决适当的绑定,这将触发倾斜动画..)李>

我会为您保存 MouseLeftDown/Up 代码,但如果需要,我可以提供。他们所做的就是改变DP的价值。


问题:

当我的 DependencyProperty 更新时,我不知道如何触发角度动画。好吧。这不是一个实际问题,我认为这是缺乏知识:

  • 我不知道如何捕获要与&lt;EventTrigger&gt; 一起使用的自定义事件
  • 我不知道如何以及在何处使用 True/False DependencyProperty 触发 StoryBoard。

而实际的问题是:

从现在开始,我如何声明生成 Angle 的代码 RotateTransform 的属性从 0.0 动画到 当我的 DP IsTilting 设置为 true 时,45.0(我的网格的渲染变换 "TiltingGrid"),并且动画回到 0.0 什么时候是False

主要以 XAML 方式..?

我确实在 C# 代码后面有一个工作代码(详情如下)我正在寻找的是 XAML 中的可行解决方案(因为当您在 CodeBehind 中重写几乎所有内容时通常非常容易)知道如何在 XAML 中执行此操作)


到目前为止我尝试了什么......

从现在开始,除非您绝对想知道所有细节,否则您无需再阅读更多内容...


1) 使用本机定义的 Ellipse EventTriggers 触发动画仅适用于为此特定 UIElement 定义的事件(Enter/Leave/MouseLeftDown...)用许多 UIElements 完成了很多。

但这些触发器不是我需要的:我的网格应该基于 DP 中的 On/OffTrue/False 自定义状态倾斜,而不是在发生鼠标活动时。

<Ellipse.Triggers>
    <EventTrigger RoutedEvent="UIElement.MouseEnter">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation 
                    Storyboard.TargetName="TiltRotate" 
                    Storyboard.TargetProperty="Angle" 
                    From="0.0" To="45.0" 
                    Duration="0:0:0.2" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
    <EventTrigger RoutedEvent="UIElement.MouseLeave">
    ...
</Ellipse.Triggers>

当鼠标进入椭圆时,我的 Grid 会相应地倾斜,但是,如何访问我的 UserControl 中定义的自定义事件?


2) 然后,基于上述方案,我想我只需要在我的 MyControl 类上创建一个 Routed Event,实际上是两个:

  • TiltingActivated
  • TiltingDisabled

.

public static readonly RoutedEvent TiltingActivatedEvent = 
    EventManager.RegisterRoutedEvent(
        "TiltingActivated",
        RoutingStrategy.Bubble, 
        typeof(RoutedEventHandler), 
        typeof(EventHandler));

public event RoutedEventHandler TiltingActivated
{
    add { AddHandler(MyControl.TiltingActivatedEvent, value); }
    remove { RemoveHandler(MyControl.TiltingActivatedEvent, value); }
}

private void RaiseTiltingActivatedEvent()
{
    RoutedEventArgs newEventArgs = 
        new RoutedEventArgs(MyControl.TiltingActivatedEvent, this);
    RaiseEvent(newEventArgs);
}

然后,当 IsTilting DependencyProperty Callback 的新值为 trueRaiseTiltingDisabledEvent() 当其新值为 时,我在一个方法中调用 RaiseTiltingActivatedEvent() >false.

注意:IsTilting 值在椭圆“单击”时更改为 true 或 false,并相应地触发两个事件。但是有一个问题:触发事件的不是 Ellipse,而是 UserControl 本身。

无论如何,我尝试将&lt;EventTrigger RoutedEvent="UIElement.MouseEnter"&gt; 替换为以下内容:

尝试一:

<EventTrigger RoutedEvent="
    {Binding ic:MyControl.TiltingActivated, 
    ElementName=ThisUserControl}">

.. 我明白了:

"System.Windows.Markup.XamlParseException: (...)"
"A 'Binding' can only be set on a DependencyProperty of a DependencyObject."

我假设我无法绑定到事件?

尝试二:

<EventTrigger RoutedEvent="ic:MyControl.TiltingActivated">

.. 我明白了:

"System.NotSupportedException:"
"cannot convert RoutedEventConverter from system.string"

我假设无法解析 RoutedEvent 名称?无论如何,这种方法让我偏离了我最初的目标:当自定义属性发生变化时触发 DoubleAnimation(因为在更复杂的场景中,触发不同的动画和调用特定的方法不是更容易,所有这些都在 CodeBehind 当我们可以有几十个不同的值,而不是创建冗长而棘手的 XAML 东西?最好的当然是学习如何做这两个。我很想知道)


3) 然后我遇到了this article:初学者的WPF动画教程。

代码背后动画创作。这就是我想知道如何在 XAML 中做到这一点之后学习的东西。无论如何,让我们试一试。

a) 创建两个动画属性(私有),一个用于倾斜动画,另一个用于向后倾斜动画。

private DoubleAnimation p_TiltingPlay = null;
private DoubleAnimation TiltingPlay
{
    get {
        if (p_TiltingPlay == null) {
            p_TiltingPlay = 
                new DoubleAnimation(
                    0.0, 45.0, new Duration(TimeSpan.FromSeconds(0.2)));
        }
        return p_TiltingPlay;
    }
}
// Similar thing for TiltingReverse Property...

b) 订阅这两个事件,然后在运行时在后面的代码中设置我们的 RotateTransform 的角度动画:

private void MyControl_TiltingActivated(object source, EventArgs e)
{
    TiltRotate.BeginAnimation(
        RotateTransform.AngleProperty, TiltingPlay);
}

// Same thing for MyControl_TiltingDisabled(...)

// Subscribe to the two events in constructor...
public MyControl()
{
    InitializeComponent();
    this.TiltingActivated += 
        new RoutedEventHandler(MyControl_TiltingActivated);
    this.TiltingDisabled += 
        new RoutedEventHandler(MyControl_TiltingDisabled);
}

基本上,当我在椭圆上“单击”(MouseButtonLeftDown + Up)时:

  • 鼠标点击点已解决
  • 如果在椭圆区域内,请将 DP IsTilting 更改为 not IsTilting。
  • IsTilting 然后触发 TiltingActivated 或 TiltingDisabled。
  • 两者都被捕获,然后在网格的命名&lt;RotateTransform ..&gt; 上激活相关的倾斜动画(私有属性)。

而且有效!!!

我说后面的代码很容易! (冗长的代码 .. 是的,但它有效)希望使用 sn-ps 模板,它不会那么无聊。


但我仍然不知道如何在 XAML 中执行此操作。 :/

4) 由于我的自定义事件似乎超出了 XAML 端的范围,那么 &lt;Style&gt; 呢? 通常,在 Style 中绑定就像呼吸一样。但老实说,我不知道从哪里开始。

  • 动画目标是应用于Grid&lt;RotateTransform /&gt;Angle 属性。
  • 绑定的部门。属性 IsTilting 是 MyControl 的自定义 DP,而不是 UserControl
  • 和一个Ellipse驱动DP的更新。

让我们试试&lt;RotateTransform.Style&gt;

<RotateTransform ...>
    <RotateTransform.st...>
</RotateTransform>
<!-- such thing does not exists -->

RotateTransform.Triggers ? ...也不存在。

更新: 这种方法通过将网格中的样式声明为动画,如Clemens's answer 中所述。解决自定义 UserControl 属性绑定,我只需要使用 RelativeSource={RelativeSource AncestorType=UserControl}}。并 “瞄准” RotateTransform 的角度属性,我只需要使用 RenderTransform.Angle.


还有什么?

  1. 我经常看到将DataContext 设置为“self”之类的示例。我真的不明白什么是 DataContext,但我假设它使所有路径解析默认指向声明的类,用于绑定。我已经在一个解决了我的问题的 UserControl 中使用了它,但我没有深入了解如何以及为什么。也许这可以帮助解决直接从 XAML 端捕获代码中的自定义事件?

  2. 一种 XAML 主要方式我几乎可以肯定会起作用的是:

    • 为该椭圆创建一个自定义 UserControl,例如 EllipseButton,并具有自己的事件和属性
    • 然后,将其嵌入MyControl UserControl。
    • 捕获 EllipseButton 的 TiltingActivated 事件以在 EllipseButton 的 Storyboard 中触发 DoubleAnimation,就像对 Button 的 Click 事件一样。

    这很好用,但我发现 hacky 创建 并嵌入 另一个控件 只是为了能够访问适当的自定义事件. MyControl 不是一个需要这种手术的 SuperWonderfulMegaTop 项目。我确定我错过了一些非常明显的东西;简直不敢相信在 WPF 世界之外这么简单的东西在 WPF 中就更简单了。

    无论如何,这样的交叉连接很容易发生内存泄漏(这里可能不是这种情况,但我尽量避免这种情况......)

  3. 也许定义&lt;Grid.Style&gt; 或类似的方法可以解决问题……但我不知道怎么做。我只知道怎么用&lt;Setter&gt;。我不知道如何在 Style 声明中创建 EventTriggers。 更新:Clemens's answer解释。

  4. SO question(基于 DependencyProperty 的 UserControl 中的触发触发器)建议在 UserControl.Resources 中创建样式。尝试了以下...它不起作用(反正那里没有动画 - 我还不知道如何在 Style 中声明动画)

.

<Style TargetType="RotateTransform">
    <Style.Triggers>
        <DataTrigger
            Binding="{Binding IsTilting, ElementName=ThisUserControl}" Value="True">
            <Setter Property="Angle" Value="45.0" />
        </DataTrigger>
    </Style.Triggers>
</Style>
  1. 这个SO question(DataTemplate 中的 RotateTransform Angle 绑定不生效)对我来说有很多未知的知识可以理解。但是,假设建议的解决方法有效,我看不到任何看起来像动画的东西。只是绑定到没有动画的值。我不认为角度会神奇地为自己设置动画。

  2. 在像上面的工作代码一样的代码后面,我可以创建另一个名为 GridAngle (double) 的 DependencyProperty,然后将 RotateTransform 的 Angle 属性绑定到该新 DP,然后直接为该 DP 设置动画???值得一试,但稍后:我累了。

  3. 刚刚发现我的注册事件属于冒泡策略。如果事件要被某些父容器捕获,这将很重要,但我想直接在 UserControl 内处理所有内容,而不是像 SO question 那样。但是,我还不了解的隧道策略可能会起作用:隧道是否允许我的 Ellipse 捕获我的 UserControl 的事件?必须一遍又一遍地阅读文档,因为它对我来说仍然很模糊......现在让我感到烦恼的是我仍然无法在这个 UserControl 中使用我的自定义事件:/

  4. a CommandBinding 呢?这看起来很有趣,但这是一个完全不同的章节来学习。它似乎涉及很多代码,因为我已经有一个工作代码(对我来说看起来更具可读性......)

  5. 在这个SO question(WPF 数据触发器和故事板)中,接受的答案似乎只有在我为可以具有UIElement.Style 定义的 UI 元素的属性设置动画时才有效。 RotateTransform没有这种能力。

    另一个答案建议使用 ContentControlControlTemplate... 就像上面的 CommandBinding 一样,我还没有深入了解如何使其适应我的 UserControl。

    但是,这些答案似乎最符合我的需求,尤其是 ContentControl 方式。稍后我会进行一些尝试,看看它是否解决了实现所需行为的 XAML 主要方式。 :)

  6. 最后,这个SO question(EventTrigger 绑定到来自 DataContext 的事件)建议使用Blend/Interactivity。这种方法看起来不错,但我没有 Blend SDK,也不是真的愿意,除非我绝对必须...再次:又一整章吃...:/


旁注:

如您所料,我是 WPF/XAML 的初学者(我知道这不是借口),我几周前就开始学习了。我有点“现在在 WinForms 中很容易完成所有事情......” 但也许你可以帮我弄清楚在 WPF 中实现它是多么容易:)

我搜索了很多(我知道这也不是借口),但这次我没有运气。 - 好的,我刚刚阅读了三打文章,代码项目和 SO 主题,以及关于触发器、动画、路由事件的 MSDN 文档.. 似乎只是打磨表面而没有深入挖掘核心(似乎 MS 认为继承from Button 是解决几乎所有问题的方法...)

【问题讨论】:

    标签: c# wpf xaml eventtrigger rotatetransform


    【解决方案1】:

    长问题,短答案。使用Visual States:

    <UserControl ...>
        <Grid>
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup>
                    <VisualState x:Name="TiltedState">
                        <Storyboard>
                            <DoubleAnimation
                                Storyboard.TargetName="TiltingGrid"
                                Storyboard.TargetProperty="RenderTransform.Angle"
                                To="45" Duration="0:0:0.2"/>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
            <Grid x:Name="TiltingGrid" RenderTransformOrigin="0.3, 0.5">
                <Grid.RenderTransform>
                    <RotateTransform/>
                </Grid.RenderTransform>
                ...
            </Grid>
        </Grid>
    </UserControl>
    

    只要满足适当的条件,调用

    VisualStateManager.GoToState(this, "TiltedState", true);
    

    在UserControl的代码后面。这当然也可以在依赖属性的 PropertyChangedCallback 中调用。


    不使用视觉状态,您可以为您的 TiltingGrid 创建一个样式,该样式使用一个绑定到您的 UserControl 的 IsTilted 属性的 DataTrigger:

    <Grid x:Name="TiltingGrid" RenderTransformOrigin="0.3, 0.5">
        <Grid.Style>
            <Style TargetType="Grid">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding IsTilted,
                        RelativeSource={RelativeSource AncestorType=UserControl}}"
                        Value="True">
                        <DataTrigger.EnterActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation
                                        Storyboard.TargetProperty="RenderTransform.Angle"
                                        To="45" Duration="0:0:0.2"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </DataTrigger.EnterActions>
                        <DataTrigger.ExitActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation
                                        Storyboard.TargetProperty="RenderTransform.Angle"
                                        To="0" Duration="0:0:0.2"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </DataTrigger.ExitActions>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Grid.Style>
        <Grid.RenderTransform>
            <RotateTransform/>
        </Grid.RenderTransform>
        ...
    </Grid>
    

    【讨论】:

    • 谢谢!我今天学到了很多东西。其中包括: 1) “RenderTransform.Angle”-like 属性解析。似乎可以像这样访问一些 FrameworkElements 属性。 2)VisualStatesManager;这是我今天获得的一个重要的新学习工具,我在其他搜索结果中没有找到(或错过)。您的两个建议都有效(而我不得不将 .GoToState(this, ...) 更改为 .GoToElementState(TiltingGrid, ...)
    • 我也明白,为了影响 UIElement 的渲染转换,我必须在该 UIElement 中声明 Style.Triggers。我只习惯在引发事件的 UIElement 中声明 EventTriggers。尽管如此,我仍然无法在 UserControl 本身中捕获我的自定义事件,这仍然让我感到困扰 XD。不管怎样,谢谢你。这个答案将帮助我在我制作的大量用户控件中杀死 CodeBehind(并将)
    • 谢谢@Clemens,但是你如何使用 VisualStateManager.GoToState(this, "TiltedState", true);当“this”不是静态时,来自依赖属性的 PropertyChangedCallback ?
    • 忽略,我让它与 VisualStateManager.GoToState(d as FrameworkElement, "LoadingState", true);
    • @federom 没有必要在另一条评论中解释这一点。最好只删除两个 cmets。
    猜你喜欢
    • 1970-01-01
    • 2020-06-16
    • 2013-01-20
    • 1970-01-01
    • 1970-01-01
    • 2012-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多