c # - WPF Bind Canvas.Left/Canvas.Top til Point DependencyProperty, Use PointAnimation

Indlæg af Hanne Mølgaard Plasc

Problem



Overvej følgende forenklet eksempel, som illustrerer mit problem:


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


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


<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


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


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


Målene her er:



  1. Brug LocationProperty til at manipulere og få adgang til værdierne Canvas.LeftProperty og Canvas.TopProperty.

  2. Angiv sagt LocationProperty med klassen PointAnimation.



Mål # 1 ser ud til at fungere korrekt, det er kun, når man forsøger at animere LocationProperty, opfører sig det ikke som forventet.


Med 'forventet' mener jeg, at forekomsten af ​​Thingy skal bevæge sig, når animationen skrider frem.


Jeg er i stand til at opnå dette ved hjælp af to tilfælde af DoubleAnimation klassen.


Hvis problemet er, at Point er en værdi type, så formoder jeg, at jeg kunne definere min egen Point type og min egen AnimationTimeline. Dette er ikke det, jeg ønsker at gøre. Dette er en del af et meget større projekt, og LocationProperty vil blive brugt til andre ting.


Og for at være ærlig er bundlinjen, at det forekommer mig, at dette bare skal fungere, kan du fortælle mig det:



  1. Hvorfor det ikke?

  2. Hvis der er en løsning på problemet som defineret?



Jeg vil også nævne, at jeg målretter mod. Net Framework 4.5 for dette projekt.


Tak skal du have.

Bedste reference


Her er den enkleste kode til at animere noget.



  • Det udnytter afhængighedsegenskabens tilbagekald

  • bruger ikke en bindende

  • bruger ikke en konverter

  • bruger ikke et storyboard



Hovedvindue:


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);
        }
    }
}


Hovedvindue:


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


Kontrollere:


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);
        }
    }
}


Kontrollere:


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


FORDO: juster koden til dine behov :)


EDIT: En triviel tovejsbinding, der lytter til Canvas.[Left|Top]Property:


(skal forbedres)


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);
        }
    }
}

Andre referencer 1


Jeg kan godt lide Aybe svar, men det taler ikke om, hvorfor den oprindelige kode ikke virker. Jeg kørte din kode og prøvede nogle alternativer, og det ser ud til, hvad der sker, er at bindingsomformeren ignoreres under animationen. et breakpoint i konverteringsmetoderne eller gøre en Debug.WriteLine, på den ene eller anden måde kan du se, at konverteren ikke bliver påkaldt gennem animationen, men kun når ejendommen udtrykkeligt er angivet i din kode.


Ved at grave dybere, er problemet i den måde, du etablerer bindingerne Thingy. Den bindende kilde -egenskab bør være Thingy.Location, mens målet Egenskaber skal være Canvas.Left og Canvas.Top. Du har det baglæns - du gør Canvas.Left og Canvas.Top kildeegenskaberne og Thingy.Location målegenskaben. Du ville tro, at det ville være en tovejsbinding, der ville få det til at fungere (og det gør, når du eksplicit indstiller egenskaben Thingy.Location, men det ser ud til, at det to-vejs bindende aspekt ignoreres for animationer.


En løsning er ikke at bruge en multi-binding her. Multi-binding er virkelig for, når en ejendom er hentet af flere egenskaber eller betingelser. Her har du flere egenskaber (Canvas.Left og Canvas.Top, som du vil kilde til med en enkelt ejendom - Thingy.Location. Så i Thingy konstruktøren:


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


Den anden forskel er bindingsomformeren. I stedet for at tage to doubles og give dig en Point, har vi brug for en konverter, der tager en Point og uddrag den double, der anvendes til Canvas.Left og Canvas.Top egenskaber (og jeg bruger ConverterParameter for at angive, hvilken der ønskes). Så:


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


Dette gør animationen til at fungere, mens du stadig bruger bindinger og konvertere. Den eneste ulempe her er, at bindingerne mellem Canvas egenskaberne og Thingy.Location nødvendigvis er envejs, fordi der ikke er nogen måde at konvertere Canvas.Left eller Canvas.Top alene tilbage til en fuld Point. Med andre ord, hvis du senere ændrer Canvas.Left eller Canvas.Top, vil Thingy.Location ikke opdateres. (Dette gælder også for enhver bindende-mindre løsning så godt selvfølgelig).


Men hvis du går tilbage til din originale multibindende version, og blot tilføjer koden til Location ejendomsændringshåndteringen for at opdatere Canvas.Left og Canvas.Top, kan du få din kage og spis det også. De vil ikke være TwoWay bindinger på det tidspunkt, fordi du tager dig af at opdatere Canvas.Left og Canvas.Top i ejendomsskiftehåndtereren til Location. Dybest set er al den egentlige binding det gør, er at sikre Location opdateringer når Canvas.Left og Canvas.Top gør.


Under alle omstændigheder er mysteriet løst, hvorfor din oprindelige tilgang ikke fungerede. Når du opretter komplekse bindinger, er det afgørende at identificere kilden og målene korrekt. TwoWay bindinger er ikke en catch-all for alle tilfælde, især animationer.