You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实时更新Storyboard绑定:振幅变更后动画对象路径未同步更新

问题解决:WPF正弦圆周动画路径实时更新问题

问题描述

修改ViewModel的Amplitude属性时,界面上的绿色Path图形会实时更新为新的正弦圆周,但蓝色Ellipse的运动路径仍停留在初始路径,无法同步更新。原因是DoubleAnimationUsingPath通过EventTrigger启动后,不会自动响应绑定的PathGeometry变化,必须手动更新动画实例。

解决方案

通过后台代码手动管理Storyboard动画实例,监听ViewModel的属性变化事件,在路径更新时重启动画,确保动画使用最新的PathGeometry。

修改步骤

  1. 移除XAML中Ellipse的EventTrigger,改为后台初始化并控制动画。
  2. 在后台创建Storyboard和DoubleAnimationUsingPath实例,持有引用以便后续更新。
  3. 监听ViewModel的PropertyChanged事件,当Amplitude或Points变化时,停止当前动画、更新动画的PathGeometry、重新启动动画。

修改后的代码

XAML代码

<Grid>
    <Path HorizontalAlignment="Center"
          VerticalAlignment="Center"
          Stretch="Uniform" x:Name="MainPath" SizeChanged="MainPath_SizeChanged" Fill="Green" Stroke="Black">
        <Path.Data>
            <PathGeometry x:Name="ScaledAnimationPath">
                <PathGeometry.Figures>
                    <PathFigure StartPoint="{Binding StartPoint}">
                        <PolyLineSegment Points="{Binding Points}"/>
                    </PathFigure>
                </PathGeometry.Figures>
            </PathGeometry>
        </Path.Data>
    </Path>
    <Ellipse x:Name="AnimatedEllipse" Width="30" Height="30" Fill="Blue">
        <Ellipse.RenderTransform>
            <TranslateTransform x:Name="AnimatedTranslateTransform"  />
        </Ellipse.RenderTransform>
    </Ellipse>
</Grid>

后台代码(MainWindow.xaml.cs)

public partial class MainWindow : Window
{
    private ParticlePathVM _viewModel;
    private Storyboard _animationStoryboard;
    private DoubleAnimationUsingPath _xAnimation;
    private DoubleAnimationUsingPath _yAnimation;

    public MainWindow()
    {
        InitializeComponent();
        _viewModel = new ParticlePathVM();
        DataContext = _viewModel;
        _viewModel.PropertyChanged += ViewModel_PropertyChanged;
        InitializeAnimation();
    }

    private void InitializeAnimation()
    {
        _xAnimation = new DoubleAnimationUsingPath
        {
            StoryboardTargetName = nameof(AnimatedTranslateTransform),
            StoryboardTargetProperty = "X",
            PathGeometry = ScaledAnimationPath,
            Source = PathAnimationSource.X,
            Duration = TimeSpan.FromSeconds(3)
        };

        _yAnimation = new DoubleAnimationUsingPath
        {
            StoryboardTargetName = nameof(AnimatedTranslateTransform),
            StoryboardTargetProperty = "Y",
            PathGeometry = ScaledAnimationPath,
            Source = PathAnimationSource.Y,
            Duration = TimeSpan.FromSeconds(3)
        };

        _animationStoryboard = new Storyboard
        {
            RepeatBehavior = RepeatBehavior.Forever
        };
        _animationStoryboard.Children.Add(_xAnimation);
        _animationStoryboard.Children.Add(_yAnimation);
    }

    private void RestartAnimation()
    {
        _animationStoryboard.Stop(AnimatedEllipse);
        _xAnimation.PathGeometry = ScaledAnimationPath;
        _yAnimation.PathGeometry = ScaledAnimationPath;
        _animationStoryboard.Begin(AnimatedEllipse, true);
    }

    private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(ParticlePathVM.Amplitude) || e.PropertyName == nameof(ParticlePathVM.Points))
        {
            RestartAnimation();
        }
    }

    private void MainPath_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        _viewModel.UpdateBounds(e.NewSize);
    }

    protected override void OnLoaded(EventArgs e)
    {
        base.OnLoaded(e);
        _animationStoryboard.Begin(AnimatedEllipse, true);
    }
}

ViewModel代码(保持不变)

public class ParticlePathVM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    PointCollection points;
    public PointCollection Points { get => points; set { if (points != value) { points = value; RaisePropertyChanged(nameof(Points)); } } }

    Point startPoint;
    public Point StartPoint { get => startPoint; set { if (startPoint != value) { startPoint = value; RaisePropertyChanged(nameof(StartPoint)); } } }

    double amplitude;
    public double Amplitude { get => amplitude; set { if (amplitude != value) { amplitude = value; RaisePropertyChanged(nameof(Amplitude)); UpdatePoints(); } } }

    public double AmplitudeMax { get; } = 10;

    public double Radius;
    public double Frequency;
    System.Threading.SynchronizationContext _context;

    public ParticlePathVM()
    {
        _context = System.Threading.SynchronizationContext.Current;

        Radius = 266 - AmplitudeMax;
        StartPoint = new Point(Radius, 0);
        Amplitude = 5;
        Frequency = 10;
        UpdatePoints();
    }

    public void UpdateBounds(Size s)
    {
        if (s.Width > 1)
        {
            Radius = (s.Width / 2) - AmplitudeMax;
            Debug.WriteLine(string.Format("{0} -> {1}", s, Radius));
            StartPoint = new Point(Radius, 0);
            UpdatePoints();
        }
    }

    void UpdatePoints()
    {
        var points = new List<Point>();
        for (float i = 0; i <= 360; i += 0.5F)
        {
            points.Add(GetCartesian(i));
        }
        _context.Post(o =>
        {
            Points = new PointCollection(points);
        }, null);
    }

    public Point GetCartesian(double angle)
    {
        var rad = angle * (Math.PI / 180.0);
        var r = Radius + Amplitude * Math.Sin(2 * Frequency * rad);
        return new Point(r * Math.Cos(rad), r * Math.Sin(rad));
    }
}

效果说明

修改Amplitude属性后,ViewModel会重新计算路径点并触发PropertyChanged,后台监听到后会重启动画,此时动画会使用最新的ScaledAnimationPath,蓝色Ellipse的运动路径会与绿色Path图形同步更新。

内容的提问来源于stack exchange,提问作者Eternal Ambiguity

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 02:07:13