简体   繁体   English

WPF 绑定 Canvas.Left/Canvas.Top 到 Point DependencyProperty,使用 PointAnimation

[英]WPF Bind Canvas.Left/Canvas.Top to Point DependencyProperty, Use PointAnimation

Please consider the following simplified example which illustrates my problem:请考虑以下说明我的问题的简化示例:

MainWindow.xaml主窗口.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="500" Height="500"
        Title="Click anywhere to animate the movement of the blue thingy...">
    <Canvas 
        x:Name="canvas" 
        HorizontalAlignment="Stretch" 
        VerticalAlignment="Stretch" 
        Background="AntiqueWhite"  
        MouseDown="canvas_MouseDown" />
</Window>

MainWindow.xaml.cs主窗口.xaml.cs

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Animation;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.canvas.Children.Add(new Thingy());
        }

        private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
        {
            var thingy = (Thingy)this.canvas.Children[0];

            var from = new Point(0.0, 0.0);

            var to = new Point(
                canvas.ActualWidth  - thingy.ActualWidth, 
                canvas.ActualHeight - thingy.ActualHeight
            );

            var locAnim = new PointAnimation(
                from, 
                to, 
                new Duration(TimeSpan.FromSeconds(5))
            );

            locAnim.Completed += (s, a) =>
            {
                // Only at this line does the thingy move to the 
                // correct position...
                thingy.Location = to;
            };

            thingy.Location = from;
            thingy.BeginAnimation(Thingy.LocationProperty, locAnim);
        }
    }
}

Thingy.xaml东西.xaml

<UserControl x:Class="WpfApplication1.Thingy"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Width="50" Height="50" Background="Blue" />

Thingy.xaml.cs Thingy.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfApplication1
{
    public partial class Thingy : UserControl
    {
        public static DependencyProperty LocationProperty = 
            DependencyProperty.Register(
                "Location", 
                typeof(Point), 
                typeof(Thingy)
            );

        public Thingy()
        {
            InitializeComponent();

            Canvas.SetLeft(this, 0.0);
            Canvas.SetTop(this, 0.0);

            var xBind = new Binding();
            xBind.Source = this;
            xBind.Path = new PropertyPath(Canvas.LeftProperty);
            xBind.Mode = BindingMode.TwoWay;

            var yBind = new Binding();
            yBind.Source = this;
            yBind.Path = new PropertyPath(Canvas.TopProperty);
            yBind.Mode = BindingMode.TwoWay;

            var locBind = new MultiBinding();
            locBind.Converter = new PointConverter();
            locBind.Mode = BindingMode.TwoWay;
            locBind.Bindings.Add(xBind);
            locBind.Bindings.Add(yBind);
            BindingOperations.SetBinding(
                this, 
                Thingy.LocationProperty, 
                locBind
            );
        }

        public Point Location
        {
            get
            {
                return (Point)this.GetValue(LocationProperty);
            }

            set
            {
                this.SetValue(LocationProperty, value);
            }
        }
    }
}

PointConverter.cs点转换器.cs

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication1
{
    public class PointConverter : IMultiValueConverter
    {
        public object Convert(object[] v, Type t, object p, CultureInfo c)
        {
            return new Point((double)v[0], (double)v[1]);
        }

        public object[] ConvertBack(object v, Type[] t, object p, CultureInfo c)
        {
            return new object[] { ((Point)v).X, ((Point)v).Y };
        }
    }
}

The goals here are:这里的目标是:

  1. Use the LocationProperty to manipulate and access the Canvas.LeftProperty and Canvas.TopProperty values.使用LocationProperty来操作和访问Canvas.LeftPropertyCanvas.TopProperty值。
  2. Animate said LocationProperty with the PointAnimation class. Animate 用PointAnimation类表示LocationProperty

Goal #1 appears to be working correctly, it's only when trying to animate the LocationProperty does it not behave as expected.目标 #1 似乎工作正常,只有在尝试为LocationProperty设置动画时,它才会按预期运行。

By "expected" I mean the instance of Thingy should move as the animation progresses. “预期”是指Thingy的实例应该随着动画的进行而移动。

I am able to accomplish this using two instances of the DoubleAnimation class.我可以使用DoubleAnimation类的两个实例来完成此操作。

If the problem is that Point is a value type, then I suspect that I could define my own Point type and my own AnimationTimeline .如果问题是Point是值类型,那么我怀疑我可以定义我自己的Point类型和我自己的AnimationTimeline This is not what I wish to do.这不是我想做的。 This is part of a much larger project and the LocationProperty will be used for other things.这是一个更大项目的一部分, LocationProperty将用于其他用途。

And to be honest, the bottom line is that it seems to me this should just work, can you tell me:老实说,最重要的是在我看来这应该可行,你能告诉我:

  1. Why it does not?为什么不呢?
  2. If there is a solution to the problem as defined?如果有定义的问题的解决方案?

I'll also mention that I'm targeting .Net Framework 4.5 for this project.我还要提到我的目标是 .Net Framework 4.5 用于这个项目。

Thank you.谢谢你。

Here's the simplest code to animate something.这是制作动画的最简单代码。

  • it leverages the dependency property callback它利用依赖属性回调
  • does not use a binding不使用绑定
  • does not use a converter不使用转换器
  • does not use a storyboard不使用故事板

Main window:主窗口:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;

namespace WpfApplication1
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void MainWindow_OnMouseDown(object sender, MouseButtonEventArgs e)
        {
            var x = Canvas.GetLeft(Control1);
            var y = Canvas.GetTop(Control1);
            x = double.IsNaN(x) ? 0 : x;
            y = double.IsNaN(y) ? 0 : y;
            var point1 = new Point(x, y);
            var point2 = e.GetPosition(this);
            var animation = new PointAnimation(point1, point2, new Duration(TimeSpan.FromSeconds(1)));
            animation.EasingFunction = new CubicEase();
            Control1.BeginAnimation(UserControl1.LocationProperty, animation);
        }
    }
}

Main window:主窗口:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" MouseDown="MainWindow_OnMouseDown">
    <Canvas>
        <local:UserControl1 Background="Red" Height="100" Width="100" x:Name="Control1" />
    </Canvas>
</Window>

Control:控制:

using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
    public partial class UserControl1
    {
        public static readonly DependencyProperty LocationProperty = DependencyProperty.Register(
            "Location", typeof(Point), typeof(UserControl1), new UIPropertyMetadata(default(Point), OnLocationChanged));

        public UserControl1()
        {
            InitializeComponent();
        }

        public Point Location
        {
            get { return (Point) GetValue(LocationProperty); }
            set { SetValue(LocationProperty, value); }
        }

        private static void OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control1 = (UserControl1) d;
            var value = (Point) e.NewValue;
            Canvas.SetLeft(control1, value.X);
            Canvas.SetTop(control1, value.Y);
        }
    }
}

Control:控制:

<UserControl x:Class="WpfApplication1.UserControl1"
             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" 
             xmlns:local="clr-namespace:WpfApplication1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

</UserControl>

TODO: adjust the code to your needs :) TODO:根据您的需要调整代码:)

EDIT: A trivial two-way binding that listens to Canvas.[Left|Top]Property :编辑:侦听Canvas.[Left|Top]Property简单双向绑定:

(to be enhanced) (有待加强)

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
    public partial class UserControl1
    {
        public static readonly DependencyProperty LocationProperty = DependencyProperty.Register(
            "Location", typeof(Point), typeof(UserControl1), new PropertyMetadata(default(Point), OnLocationChanged));

        public UserControl1()
        {
            InitializeComponent();

            DependencyPropertyDescriptor.FromProperty(Canvas.LeftProperty, typeof(Canvas))
                .AddValueChanged(this, OnLeftChanged);
            DependencyPropertyDescriptor.FromProperty(Canvas.TopProperty, typeof(Canvas))
                .AddValueChanged(this, OnTopChanged);
        }

        public Point Location
        {
            get { return (Point) GetValue(LocationProperty); }
            set { SetValue(LocationProperty, value); }
        }

        private void OnLeftChanged(object sender, EventArgs eventArgs)
        {
            var left = Canvas.GetLeft(this);
            Location = new Point(left, Location.Y);
        }

        private void OnTopChanged(object sender, EventArgs e)
        {
            var top = Canvas.GetTop(this);
            Location = new Point(Location.X, top);
        }

        private static void OnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control1 = (UserControl1) d;
            var value = (Point) e.NewValue;
            Canvas.SetLeft(control1, value.X);
            Canvas.SetTop(control1, value.Y);
        }
    }
}

I like Aybe's answer but it doesn't address why the original code doesn't work.我喜欢 Aybe 的回答,但它没有说明为什么原始代码不起作用。 I ran your code and tried some alternatives and it appears what is happening is that the binding converter is being ignored during the animation.我运行了您的代码并尝试了一些替代方法,看起来正在发生的事情是在动画过程中忽略了绑定转换器。 If you set a breakpoint in the converter methods, or do a Debug.WriteLine, either way you can see that the converter is not getting invoked throughout the animation, but rather only when the property is explicitly set in your code.如果您在转换器方法中设置断点,或者执行 Debug.WriteLine,无论哪种方式,您都可以看到转换器不会在整个动画过程中被调用,而是只有在代码中显式设置属性时才会被调用。

Digging deeper, the problem is in the way you're setting up the Thingy bindings.深入挖掘,问题在于您设置Thingy绑定的方式。 The binding source property should be Thingy.Location while the target properties should be Canvas.Left and Canvas.Top .绑定属性应该是Thingy.Location目标属性应该是Canvas.LeftCanvas.Top You have it backwards though - you're making Canvas.Left and Canvas.Top the source properties and Thingy.Location the target property.但是,您将其倒退了 - 您将Canvas.LeftCanvas.Top Thingy.Location源属性,将Thingy.Location目标属性。 You would think making it a two-way binding would make it work (and it does, when you explicitly set the Thingy.Location property), but it appears that the Two-Way binding aspect is ignored for animations.您可能认为将其设为双向绑定会使其工作(当您显式设置Thingy.Location属性时确实Thingy.Location ),但似乎动画忽略了双向绑定方面。

One solution is not to use a multi-binding here.一种解决方案是不要在这里使用多重绑定。 Multi-binding is really for when one property is being sourced by multiple properties or conditions.多重绑定真正适用于当一个属性由多个属性或条件获取时。 Here, you have multiple properties ( Canvas.Left and Canvas.Top ) that you want to source with a single property - Thingy.Location .在这里,您有多个属性( Canvas.LeftCanvas.Top )要使用单个属性 - Thingy.Location So, in the Thingy constructor:因此,在Thingy构造函数中:

    var xBind = new Binding();
    xBind.Source = this;
    xBind.Path = new PropertyPath(Thingy.LocationProperty);
    xBind.Mode = BindingMode.OneWay;
    xBind.Converter = new PointToDoubleConverter();
    xBind.ConverterParameter = false;
    BindingOperations.SetBinding(this, Canvas.LeftProperty, xBind);                

    var yBind = new Binding();
    yBind.Source = this;
    yBind.Path = new PropertyPath(Thingy.LocationProperty);
    yBind.Mode = BindingMode.OneWay;
    yBind.Converter = new PointToDoubleConverter();
    yBind.ConverterParameter = true;
    BindingOperations.SetBinding(this, Canvas.TopProperty, yBind); 

The other difference is the binding converter.另一个区别是绑定转换器。 Rather than take two doubles and give you a Point , we need a converter that takes a Point and extracts the double used for the Canvas.Left and Canvas.Top properties (and I'm using the ConverterParameter to specify which one is desired).我们需要一个转换器,它接受一个Point并提取用于Canvas.LeftCanvas.Top属性的double doubles ,而不是取两个doubles并给您一个Point (我使用ConverterParameter来指定需要哪一个)。 So:所以:

public class PointToDoubleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var pt = (Point)value;
        bool isY = (bool)parameter;
        return isY ? pt.Y : pt.X;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}

This makes the animation work while still using bindings and converters.这使得动画在仍然使用绑定和转换器的同时工作。 The only drawback here is that the bindings between the Canvas properties and Thingy.Location are necessarily one-way because there's no way to convert Canvas.Left or Canvas.Top alone back to a full Point .这里唯一的缺点是Canvas属性和Thingy.Location之间的绑定必然是单向的,因为无法单独将Canvas.LeftCanvas.Top转换回完整的Point In other words if you subsequently change Canvas.Left or Canvas.Top , Thingy.Location won't update.换句话说,如果您随后更改Canvas.LeftCanvas.TopThingy.Location将不会更新。 (This is true of any binding-less solution as well of course). (当然,任何无绑定解决方案也是如此)。

However, if you do go back to your original multi-binding version, and just add the code to the Location property change handler to update Canvas.Left and Canvas.Top , you can have your cake and eat it too.但是,如果您确实返回到原始的多绑定版本,并且只需将代码添加到Location属性更改处理程序以更新Canvas.LeftCanvas.Top ,您就可以吃蛋糕了。 They wouldn't need to be TwoWay bindings at that point because you're taking care of updating Canvas.Left and Canvas.Top in the property change handler for Location .此时它们不需要是TwoWay绑定,因为您正在负责更新Location的属性更改处理程序中的Canvas.LeftCanvas.Top Basically all the actual binding is then doing is making sure Location updates when Canvas.Left and Canvas.Top do.基本上所有实际的绑定都是在Canvas.LeftCanvas.Top执行时确保Location更新。

In any event, the mystery is solved as to why your original approach didn't work.无论如何,关于为什么您原来的方法不起作用的谜团已经解开了。 When setting up complex bindings it's crucial to correctly identify the source and the targets;在设置复杂绑定时,正确识别源和目标至关重要; TwoWay bindings are not a catch-all for all cases, notably, animations. TwoWay绑定并非适用于所有情况,尤其是动画。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM