none
Polylineの変更をアニメーションしたい RRS feed

  • 質問

  • 現在、MVVM形式でPoLyLineにコレクションをバインドさせて折れ線グラフを描画させています。

    ある一点の値をVM側から変更した際にPoLylineをアニメーションさせて変化させたいのですが

    どうしてもやり方がわからず詰まってしまいました。

    PoLyLineをアニメーションさせる方法は無いでしょうか?

    グラフのライブラリ等を使用するしかないのでしょうか

    2016年9月9日 21:31

回答

  • BeginAnimationでPointAnimationを渡すだけ。

    #面倒なのでMVVMもビヘイビア化もしてない。必要なら先のコードを参考に書き換えてください。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="525" Width="525">
        <Grid>
            <Canvas>
                <Canvas.LayoutTransform>
                    <ScaleTransform ScaleY="-1" />
                </Canvas.LayoutTransform>
                <Path x:Name="path" Stroke="Red"
                     Tag="{Binding .,NotifyOnTargetUpdated=True}"
                     TargetUpdated="path_TargetUpdated">
                </Path>
    
            </Canvas>
            <Button Content="Test" Click="Button_Click" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
        </Grid>
    </Window>
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using System.Windows.Media.Animation;
    using System.Collections.ObjectModel;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
    
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 10; i++)
                {
                    ps.Add(new Point(i * 50, i * 50));
                }
                this.DataContext = ps;
            }
    
            private ObservableCollection<Point> ps = new ObservableCollection<Point>();
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                Random rnd = new Random();
                ps[rnd.Next(ps.Count)] = new Point(rnd.Next(500), rnd.Next(500));
            }
    
    
    
            private void path_TargetUpdated(object sender, DataTransferEventArgs e)
            {
                var path = e.TargetObject as Path;
                var inc = path.Tag as System.Collections.Specialized.INotifyCollectionChanged;
                inc.CollectionChanged += inc_CollectionChanged;
    
                var ie = inc as IEnumerable<Point>;
                PathFigure col = new PathFigure();
                col.StartPoint = ie.First();
                foreach (Point p in ie.Skip(1))
                {
                    col.Segments.Add(new LineSegment(p, true));
                }
    
                PathGeometry pg = new PathGeometry();
                pg.Figures = new PathFigureCollection() { col };
    
                path.Data = pg;
            }
    
            private void inc_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Replace)
                {
                    Random rnd = new Random();
                    Duration d = new Duration(TimeSpan.FromSeconds(5));
    
                    PathFigure pf = ((PathGeometry)this.path.Data).Figures[0];
    
                    int index = e.NewStartingIndex;
                    foreach (Point p in e.NewItems)
                    {
    
                        PointAnimation pa = new PointAnimation(p, d);
                        if (index == 0)
                        {
                            pf.BeginAnimation(PathFigure.StartPointProperty, pa);
                        }
                        else
                        {
                            var ls = (LineSegment)(pf.Segments[index - 1]);
                            ls.BeginAnimation(LineSegment.PointProperty, pa);
                        }
                        index++;
    
                    }
                }
            }
        }
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2016年9月14日 0:50
    • 回答としてマーク WPF_Visiter 2016年9月14日 4:10
    2016年9月12日 11:21

すべての返信

  • Pathなら各点がDependencyPropertyなのでPointAnimationでアニメーション化できるんですけど、PolylineのPointsに入れるPointCollectionの要素はDependencyPropertyではないので標準ではアニメーション対象にはできないのです…

    なので、クロックを用意して各時点での位置をPointsに自前で反映させてやることになります。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="600" Width="600">
        <Window.Resources>
            <app:ColorToSolidBrush x:Key="brushConverter" />
        </Window.Resources>
        <Grid Background="LightGray">
            <ItemsControl ItemsSource="{Binding Path=Lines}" Margin="20" Background="White">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
                            <Canvas.LayoutTransform>
                                <TransformGroup>
                                    <ScaleTransform ScaleY="-1" />
                                </TransformGroup>
                            </Canvas.LayoutTransform>
                        </Canvas>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Polyline Stroke="{Binding Path=Color,Converter={StaticResource brushConverter}}" StrokeThickness="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                              app:PolylineAnimator.Points="{Binding Path=Points}"/>
                        
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
            
            <Button Content="Test" HorizontalAlignment="Center" VerticalAlignment="Bottom"
                    Command="{Binding Path=TestCommand}"/>
        </Grid>
    </Window>
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Windows.Media.Animation;
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                this.DataContext = Model.CreateTestModel();
            }
        }
    
        class Model : System.ComponentModel.INotifyPropertyChanged
        {
            public static Model CreateTestModel()
            {
                var m = new Model();
                foreach (Color c in new Color[] { Colors.Red, Colors.Blue })
                {
                    Line line = new Line();
                    line.Color = c;
                    for (int i = 0; i < 10; i++)
                    {
                        line.Points.Add(new Point(i * 50, 0));
                    }
                    m.Lines.Add(line);
                }
                return m;
            }
    
            public Model()
            {
                Lines = new ObservableCollection<Line>();
                TestCommand = new Command() { m = this };
            }
            public ObservableCollection<Line> Lines { get; private set; }
    
            public ICommand TestCommand { get; private set; }
    
            class Command : ICommand
            {
                public Model m;
    
                public bool CanExecute(object parameter) { return true; }
    
                public event EventHandler CanExecuteChanged;
    
                public void Execute(object parameter)
                {
                    Random rnd = new Random();
    
    
                    foreach (Line line in m.Lines)
                    {
                        int index = rnd.Next(line.Points.Count);
                        //for (int index = 0; index < m.Points.Count; index++)
                        {
                            line.Points[index] = new Point(line.Points[index].X, rnd.NextDouble() * 500);
                        }
                    }
                }
            }
    
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        }
        class Line
        {
            public Line()
            {
                Points = new ObservableCollection<Point>();
            }
            public ObservableCollection<Point> Points { get; private set; }
            public Color Color { get; set; }
        }
        class ColorToSolidBrush : System.Windows.Data.IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                try
                {
                    return new SolidColorBrush((Color)value);
                }
                catch
                {
                    return null;
                }
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                SolidColorBrush brush = value as SolidColorBrush;
                if (brush != null)
                {
                    return brush.Color;
                }
                throw new NotSupportedException();
            }
        }
    
    
        class PolylineAnimator
        {
            public static IEnumerable<Point> GetPoints(DependencyObject obj)
            {
                return (IEnumerable<Point>)obj.GetValue(PointsProperty);
            }
    
            public static void SetPoints(DependencyObject obj, IEnumerable<Point> value)
            {
                obj.SetValue(PointsProperty, value);
            }
    
            public static readonly DependencyProperty PointsProperty =
                DependencyProperty.RegisterAttached("Points", typeof(IEnumerable<Point>), typeof(PolylineAnimator)
                , new FrameworkPropertyMetadata(null, OnPointsChanged));
    
            private static readonly DependencyProperty AnimatorProperty = DependencyProperty.RegisterAttached("Animator", typeof(PolylineAnimator), typeof(PolylineAnimator), new PropertyMetadata(null));
    
            private static void OnPointsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                Polyline poly = d as Polyline;
                if (poly == null)
                {
                    return;
                }
    
                var animator = (PolylineAnimator)poly.GetValue(AnimatorProperty);
                if (animator != null)
                {
                    animator.Disconnect();
                    poly.ClearValue(AnimatorProperty);
                }
                if (e.NewValue != null)
                {
                    var ie = e.NewValue as IEnumerable<Point>;
                    if (ie != null)
                    {
                        poly.Points = new PointCollection(ie);
    
                        var inc = ie as INotifyCollectionChanged;
                        if (inc != null)
                        {
                            animator = new PolylineAnimator(poly, inc);
                            poly.SetValue(AnimatorProperty, animator);
                        }
                    }
                }
            }
    
    
    
            private Duration duration;//アニメーション時間
            private Polyline Polyline;//操作対象
            private INotifyCollectionChanged SourceCollection;//PolylineのPointsに渡す前のPointのコレクション
            private Dictionary<AnimationClock, AnimationItem> dic = new Dictionary<AnimationClock, AnimationItem>();
    
            public PolylineAnimator(Polyline poly, INotifyCollectionChanged sourceCollection)
            {
                this.Polyline = poly;
                this.SourceCollection = sourceCollection;
                this.SourceCollection.CollectionChanged += OnCollectionChanged;//本当はWeakeventでするべき
    
                this.duration = new Duration(TimeSpan.FromSeconds(3));
            }
            public void Disconnect()
            {
                foreach (var item in this.dic.Values)
                {
                    item.Stop();
                }
                this.dic.Clear();
    
                this.SourceCollection.CollectionChanged -= OnCollectionChanged;
            }
    
    
            public void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
            {
                //ソースのコレクションのPointが操作された
                if (e.Action == NotifyCollectionChangedAction.Replace)
                {
                    int startIndex = e.NewStartingIndex;
                    int endIndex = e.NewStartingIndex + e.NewItems.Count - 1;
    
                    Dictionary<int, Point> currentPoints = new Dictionary<int, Point>();
                    foreach (AnimationClock ac in dic.Keys.ToArray())
                    {
                        //操作対象のインデックスが重なっていたら前の操作から外す
                        var item = dic[ac];
                        for (int i = item.Animations.Count - 1; i >= 0; --i)
                        {
                            IndexedPointAnimation ipa = item.Animations[i];
                            if (startIndex <= ipa.Index && ipa.Index <= endIndex)
                            {
                                Point p = item.GetCurrentPoint(ipa);
                                currentPoints.Add(ipa.Index, p);
                                item.Animations.RemoveAt(i);
                            }
                        }
    
                        if (item.Animations.Count == 0)
                        {
                            dic.Remove(ac);
                            ac.Controller.Stop();
                            ac.Controller.Remove();
                        }
                    }
    
                    AnimationItem animationItem = null;
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        //操作されたPointのアニメーションを作る
                        int index = i + e.NewStartingIndex;
    
                        Point old = (Point)e.OldItems[i];
                        if (currentPoints.ContainsKey(index))
                        {
                            old = currentPoints[index];//アニメーション中なら現在位置からアニメーションさせる
                        }
    
                        var ipa = new IndexedPointAnimation(index, old, (Point)e.NewItems[i], this.duration);
                        if (i == 0)
                        {
                            var ac = ipa.CreateClock();
                            animationItem = new AnimationItem(this, ac);
                            dic.Add(ac, animationItem);
                        }
                        animationItem.Animations.Add(ipa);
                    }
    
                }
                else
                {//複雑な変更には対応してません
    
                    foreach (var item in this.dic.Values)
                    {
                        item.Stop();
                    }
                    this.dic.Clear();
    
                    var ie = Polyline.Tag as IEnumerable<Point>;
                    if (ie != null)
                    {
                        Polyline.Points = new PointCollection(ie);
                    }
                }
            }
    
            /// <summary>アニメーション操作用</summary>
            class AnimationItem
            {
                public AnimationItem(PolylineAnimator parent, AnimationClock ac)
                {
                    this.parent = parent;
                    this.PointCollection = parent.Polyline.Points;
                    this.Clock = ac;
                    ac.CurrentTimeInvalidated += ac_CurrentTimeInvalidated;
                    ac.Completed += ac_Completed;
                }
                public event EventHandler Completed;
                private PolylineAnimator parent;
                private PointCollection PointCollection;
                private AnimationClock Clock;
                public readonly List<IndexedPointAnimation> Animations = new List<IndexedPointAnimation>();
    
                public Point GetCurrentPoint(IndexedPointAnimation ipa)
                {
                    return (Point)ipa.GetCurrentValue(ipa.From, ipa.To, this.Clock);
                }
    
                public void ac_CurrentTimeInvalidated(object sender, EventArgs e)
                {//アニメーションで時間経過
                    Update();
                }
                public void ac_Completed(object sender, EventArgs e)
                {//アニメーション完了
                    Update();
                    parent.dic.Remove(this.Clock);
                }
    
                private void Update()
                {
                    //アニメーション結果を反映
                    foreach (IndexedPointAnimation ipa in Animations)
                    {
                        PointCollection[ipa.Index] = GetCurrentPoint(ipa);
                    }
                }
                public void Stop()
                {
                    this.Clock.Controller.Remove();
                    parent.dic.Remove(this.Clock);
                }
            }
    
            /// <summary>操作するIndexを持っているPointAnimation</summary>
            class IndexedPointAnimation : PointAnimation
            {
                public IndexedPointAnimation(int index, Point fromPoint, Point toPoint, Duration d)
                    : base(fromPoint, toPoint, d)
                {
                    this.Index = index;
                }
                public int Index { get; private set; }
            }
        }
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2016年9月11日 13:42 PolylineのPointsの説明が不十分だったので追記
    2016年9月10日 9:55
  • gekka様 早速のご回答ありがとうございます。

    PolyLineをアニメーションさせるのは厳しいのですね。

    アニメーションさせるだけに対しては随分とコードが増えるので、少し腰がひけています。

    PathでもLineSegment を用いれば折れ線グラフを作成することが可能なので、

    こちらに変更すれば、PointAnimationを使用して、より簡単にアニメーションをする事が可能なのでしょうか?

    2016年9月12日 7:10
  • BeginAnimationでPointAnimationを渡すだけ。

    #面倒なのでMVVMもビヘイビア化もしてない。必要なら先のコードを参考に書き換えてください。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:app="clr-namespace:WpfApplication1"
            Title="MainWindow" Height="525" Width="525">
        <Grid>
            <Canvas>
                <Canvas.LayoutTransform>
                    <ScaleTransform ScaleY="-1" />
                </Canvas.LayoutTransform>
                <Path x:Name="path" Stroke="Red"
                     Tag="{Binding .,NotifyOnTargetUpdated=True}"
                     TargetUpdated="path_TargetUpdated">
                </Path>
    
            </Canvas>
            <Button Content="Test" Click="Button_Click" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
        </Grid>
    </Window>
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using System.Windows.Media.Animation;
    using System.Collections.ObjectModel;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
    
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 10; i++)
                {
                    ps.Add(new Point(i * 50, i * 50));
                }
                this.DataContext = ps;
            }
    
            private ObservableCollection<Point> ps = new ObservableCollection<Point>();
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                Random rnd = new Random();
                ps[rnd.Next(ps.Count)] = new Point(rnd.Next(500), rnd.Next(500));
            }
    
    
    
            private void path_TargetUpdated(object sender, DataTransferEventArgs e)
            {
                var path = e.TargetObject as Path;
                var inc = path.Tag as System.Collections.Specialized.INotifyCollectionChanged;
                inc.CollectionChanged += inc_CollectionChanged;
    
                var ie = inc as IEnumerable<Point>;
                PathFigure col = new PathFigure();
                col.StartPoint = ie.First();
                foreach (Point p in ie.Skip(1))
                {
                    col.Segments.Add(new LineSegment(p, true));
                }
    
                PathGeometry pg = new PathGeometry();
                pg.Figures = new PathFigureCollection() { col };
    
                path.Data = pg;
            }
    
            private void inc_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Replace)
                {
                    Random rnd = new Random();
                    Duration d = new Duration(TimeSpan.FromSeconds(5));
    
                    PathFigure pf = ((PathGeometry)this.path.Data).Figures[0];
    
                    int index = e.NewStartingIndex;
                    foreach (Point p in e.NewItems)
                    {
    
                        PointAnimation pa = new PointAnimation(p, d);
                        if (index == 0)
                        {
                            pf.BeginAnimation(PathFigure.StartPointProperty, pa);
                        }
                        else
                        {
                            var ls = (LineSegment)(pf.Segments[index - 1]);
                            ls.BeginAnimation(LineSegment.PointProperty, pa);
                        }
                        index++;
    
                    }
                }
            }
        }
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2016年9月14日 0:50
    • 回答としてマーク WPF_Visiter 2016年9月14日 4:10
    2016年9月12日 11:21
  • gekkaさんありがとうございます

    随分楽になりそう?なイメージです

    頂いたソースを元にチャレンジしてみたいと思います。

    もしかたしたら、また質問してしまうかもしれませんが・・・

    2016年9月12日 14:43
  • gekkaさん

    ありがとうございました。

    頂いたサンプルを元になんとか作る事ができました。

    自分の至らなさで

    データ量が多い場合に(10000プロット位)アニメーションに時間がかかったり、

    ViewModelにPathGeometryのプロパティを持たしたりで、出来の良くないプログラムになってしまいましたが

    何とか改善していこうと思います。ありがとうございました

    2016年9月14日 4:10