属性 由用户控件中的自定义依赖项属性 触发的值动画?

Property Value Animation triggered by a custom DependencyProperty in UserControl?

问题前奏:

How can I animate the Angle Property of a RotateTransform of an UIElement A when the value of a Custom DependencyProperty of type boolean becomes True when I click on an UIElement B, all inside an UserControl ? And in XAML ONLY (or mostly) ? if possible :)

我写了以下所有内容以提供 所有 我的问题所需的详细信息。您可以随时停止从上到下阅读;甚至直接跳到正题,在post.

的第一节内

上下文:

问题是关于 Animation TriggersCustom 属性 Binding,都在一个 UserControl 中。到目前为止没有 Window 涉及。

首先,假设我创建了一个 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>

然后,在代码隐藏中,我有一个名为 IsTilting.

DependencyProperty
// 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 创建两个事件处理程序。我必须这样做才能:

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


问题:

我不知道如何在我的依赖项属性更新时触发角度动画。出色地。这不是一个实际问题,我认为这是缺乏知识:


And the actual question is :

From now on, how do I declare the code that makes the Angle Property of the RotateTransform to animate from 0.0 to 45.0 (Rendering Transform of my Grid "TiltingGrid") when my DP IsTilting is set to true, and animate back to 0.0 when it's False ?

mostly in XAML way ..?

我在 C# 代码后面确实有一个工作代码(详见下文)我正在寻找的是 XAML 中的一个可行解决方案(因为它通常非常当您知道如何在 XAML)

中重写代码隐藏中的几乎任何内容时,都可以轻松重写

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

From now on, you don't have to read further unless you absolutely want to know all the details...


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

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

<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>

当鼠标进入椭圆时,我的网格会相应地倾斜,但因此,我如何才能访问我的用户控件中定义的自定义事件?


2) 然后,根据上面的方案,我想我只需要在我的 MyControl Class 上创建一个 Routed Event ,或两个,实际上:

.

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 依赖项 属性 调用的一个方法中调用 RaiseTiltingActivatedEvent() 回调,当它的新值为 true, 和 RaiseTiltingDisabledEvent() 当它的新值是 false.

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

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

尝试一:

<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动画教程

A 代码隐藏 动画创建。这就是我想在 知道如何在 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);
}

基本上,当我在椭圆上 "click" (MouseButtonLeftDown + Up) 时:

And it works !!!

I said it would be very easy in code behind ! (lengthy code .. yes, but it works) Hopefully, with snippets templates, it's not that boring.


但是在XAML中我还是不知道怎么做。 :/

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

让我们试试 <RotateTransform.Style>

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

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

UPDATE : This approach works by declaring the Style in the Grid to animate, as explained in . To resolve the custom UserControl Property binding, I just had to use RelativeSource={RelativeSource AncestorType=UserControl}}. And to "target" the Angle Property of the RotateTransform, I just had to use RenderTransform.Angle.


还有什么?

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

  2. 一个 XAML 大多数方式 我几乎可以肯定会工作的是 :

    • 为该 Ellipse 创建自定义用户控件,例如 EllipseButton,具有自己的事件和属性
    • 然后,将其嵌入 MyControl UserControl。
    • 捕获 EllipseButton 的 TiltingActivated 事件以触发 EllipseButton 的 Storyboard 中的 DoubleAnimation,就像可以为 Button 的 Click 事件完成一样。

    那会很好,但我发现 hackycreate 并嵌入 另一个控件 只是为了能够访问适当的自定义事件。 MyControl 不是需要进行此类手术的 SuperWonderfulMegaTop 项目。我确定我错过了一些非常明显的东西;无法相信在 WPF 世界之外如此简单的东西在 WPF 世界中甚至不能更简单。

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

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

  4. 这个SO question(基于Dependency属性在UserControl中触发触发器)建议在UserControl.Resources中创建一个Style。尝试了以下...它不起作用(而且那里没有动画 - 我还不知道如何在 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(Binding on RotateTransform Angle in DataTemplate not eating effect)有很多我不知道的知识,可以理解。但是,假设建议的解决方法有效,我看不到任何看起来像动画的东西。只是绑定到一个没有动画的值。我不认为角度会神奇地动画化自己。

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

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

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

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

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

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

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


旁注:

如您所料,我是几周前开始学习的 WPF/XAML 的初学者(我知道这不是借口)。我有点 "the whole stuff would be very easy to do in WinForms right now..." 但也许你能帮我弄清楚在 WPF 中实现它是多么容易:)

我搜索了很多(我知道这也不是借口)但是这次我没有运气。 - 好吧,我刚刚阅读了三打文章、代码项目和 SO 主题,以及关于触发器、动画、路由事件的 MSDN 文档。.来自 Button 是解决几乎所有问题的方法...)

长问短答。使用 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);

在用户控件后面的代码中。这当然也可以在依赖项 属性.

的 PropertyChangedCallback 中调用

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

<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>