C# 在WPF中拖动对象时如何实现平滑动画效果

C# 在WPF中拖动对象时如何实现平滑动画效果,c#,wpf,C#,Wpf,我正在尝试学习一些WPF,我希望能够实现一个简单的游戏。在这个游戏中,画布上有一些物品。对于这个问题,假设只有一个,它是一个椭圆: <Canvas Name="canvas"> <Ellipse Name="ellipse" Width="100" Height="100" Stroke="Black" StrokeThickness="3" Fill="GreenYellow"/> </Canvas> 现在,如果你试试这个,你会发现动作非常参差不齐

我正在尝试学习一些WPF,我希望能够实现一个简单的游戏。在这个游戏中,
画布上有一些物品。对于这个问题,假设只有一个,它是一个
椭圆

<Canvas Name="canvas">
    <Ellipse Name="ellipse" Width="100" Height="100" Stroke="Black" StrokeThickness="3" Fill="GreenYellow"/>
</Canvas>
现在,如果你试试这个,你会发现动作非常参差不齐。当您按下鼠标时,椭圆会立即增长并立即更改其透明度。我想把这一点弄平滑,这样就不会有突然的跳跃。

我在
Ellipse.OpacityProperty
ScaleTransform.ScaleProperty
和(特别是)Canvas.LeftProperty/TopProperty上使用了
DoubleAnimation
,尝试了一些显而易见的事情。但是,我遇到了以下问题:

  • 一旦我在
    Canvas.LeftProperty/TopProperty
    上开始动画,我就不能再使用
    Canvas.SetLeft/Top
    ,因此椭圆在拖动时不会移动。我找不到从对象中删除动画的方法

  • 如果用户在动画仍在进行时释放鼠标,
    ScaleTransform
    上的“收缩”动画在“增长”动画到达之前从完整大小开始,这会导致突然跳跃。如果疯狂地单击鼠标,对象的大小会疯狂地跳跃,而它不应该这样做

如果你需要,你可以

如何在WPF中正确执行这些平滑动作?


请不要在没有尝试的情况下发布答案。如果有任何突然的跳跃,结果是不令人满意的。谢谢

您可能应该查看控件


这里有一个使用它的例子

你可以得到一些带有特效的动画,这会软化最初的拖动效果。。。。我担心拖拽动画可能不会平滑,因为WPF不仅仅是为这类事情制作的,我认为你应该选择XNA:p

此视频可能适合您的需要:

不要为每次更改创建新的ScaleTransform,而是使用相同的ScaleTransform并继续应用新动画。如果不为动画指定
From
属性,它将从当前值开始并执行平滑动画

为避免位置跳过,请记住鼠标在椭圆内的位置,而不是始终将其居中。这样你就不用担心重新居中了。(您可以使用空时间线调用BeginAnimation来停止当前动画,但您只需在第一个鼠标移动中跳转即可。)

在XAML中:

<Ellipse Name="ellipse" Width="100" Height="100"
         Stroke="Black" StrokeThickness="3" Fill="GreenYellow">
    <Ellipse.RenderTransform>
        <ScaleTransform x:Name="scale" CenterX="50" CenterY="50"/>
    </Ellipse.RenderTransform>
</Ellipse>

如何将
Canvas.SetLeft
返回到 手术后的正常操作
画布上的动画。LeftProperty

一种方法是将设置为停止:

ellipse.BeginAnimation(Canvas.LeftProperty, new DoubleAnimation(
    pos.X - ellipse.Width * 0.5, 
    new Duration(TimeSpan.FromSeconds(1)), 
    FillBehavior.Stop));
Canvas.SetLeft(ellipse, pos.X - ellipse.Width * 0.5);
这将导致属性在动画结束后返回其未设置动画的值。如果在启动动画后设置该值,则未设置动画的值将只是最终值

另一种方法是在完成后清除动画:

ellipse.BeginAnimation(Canvas.LeftProperty, null);
不过,当你拖动时,这两个选项中的任何一个都会导致它跳跃。您可以让拖动每次启动一个新动画,但这会使拖动感觉非常滞后。也许您想使用Canvas.Left处理拖动,但是使用动画TranslateTransform处理平滑居中

XAML:

<Ellipse.RenderTransform>
    <TransformGroup>
        <ScaleTransform x:Name="scale" CenterX="50" CenterY="50"/>
        <TranslateTransform x:Name="translate"/>
    </TransformGroup>
</Ellipse.RenderTransform>

正如Quartermeister已经提到的,您不应该为动画指定
from
值。这样,动画将从当前值开始,并与当前正在执行的动画相结合。此外,您不应该每次都重新创建转换

除此之外,我建议您使用
TranslateTransform
而不是设置
Canvas
Top
/
Left
属性。它为您提供了移动灵活性,而且您不必绑定到画布面板

下面是我得到的:

XAML:

<Canvas Name="canvas">
    <Ellipse Name="ellipse" Width="100" Height="100" Stroke="Black" StrokeThickness="3" Fill="GreenYellow"
             RenderTransformOrigin="0.5,0.5">
        <Ellipse.RenderTransform>
            <TransformGroup>
                <ScaleTransform />
                <TranslateTransform />
            </TransformGroup>                
        </Ellipse.RenderTransform>
    </Ellipse>
</Canvas>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Canvas.SetLeft(ellipse, 0);
        Canvas.SetTop(ellipse, 0);

        ellipse.MouseDown += new MouseButtonEventHandler(ellipse_MouseDown);
        ellipse.MouseMove += new MouseEventHandler(ellipse_MouseMove);
        ellipse.MouseUp += new MouseButtonEventHandler(ellipse_MouseUp);
    }

    private ScaleTransform EllipseScaleTransform
    {
        get { return (ScaleTransform)((TransformGroup)ellipse.RenderTransform).Children[0]; }
    }

    private TranslateTransform EllipseTranslateTransform
    {
        get { return (TranslateTransform)((TransformGroup)ellipse.RenderTransform).Children[1]; }
    }

    void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed)
            return;

        ellipse.CaptureMouse();
        var pos = e.GetPosition(canvas);

        AnimateScaleTo(1.25);
    }

    void ellipse_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
            return;

        var pos = e.GetPosition(canvas);

        AnimatePositionTo(pos);
    }

    void ellipse_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (!ellipse.IsMouseCaptured)
            return;
        ellipse.ReleaseMouseCapture();

        AnimateScaleTo(1);
    }

    private void AnimateScaleTo(double scale)
    {
        var animationDuration = TimeSpan.FromSeconds(1);
        var scaleAnimate = new DoubleAnimation(scale, new Duration(animationDuration));
        EllipseScaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
        EllipseScaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);
    }

    private void AnimatePositionTo(Point pos)
    {
        var xOffset = pos.X - ellipse.Width * 0.5;
        var yOffset = pos.Y - ellipse.Height * 0.5;

        var animationDuration = TimeSpan.FromSeconds(1);

        EllipseTranslateTransform.BeginAnimation(TranslateTransform.XProperty,
            new DoubleAnimation(xOffset, new Duration(animationDuration)));

        EllipseTranslateTransform.BeginAnimation(TranslateTransform.YProperty,
            new DoubleAnimation(yOffset, new Duration(animationDuration)));
    }
}

为什么要投否决票?这正是他想要的:pI不明白为什么这个答案被否决了,我使用了代码项目文章中显示的方法,它似乎完全符合这个问题的要求。代码项目页面如何回答这个问题而不提及动画?即使我用拇指,那又如何回答任何问题呢?所有这些都依然存在。将我的答案与此混合,你就会得到你的应用:)动画处理平滑最初的拖动效果,这会平滑你的拖动。这大约是我第十次听说“WPF不是为这种事情而设计的”。如果WPF中的动画API连最基本的动画都做不到,那么它的用途是什么呢?不要因为提到“游戏”就破坏了你对它的理解。“游戏”并不自动意味着“XNA”。如果我问“我如何在我的WPF游戏中制作一个按钮”,你肯定不会立即建议XNA?…非常感谢,迄今为止最好的答案。。。但是,您避免了使用鼠标将对象居中的问题,而不是实际解决它。问题仍然存在:在
Canvas.LeftProperty
上有动画后,如何将
Canvas.SetLeft
返回到正常操作?我怀疑在您的解决方案中,
scale.ScaleX
也会出现同样的问题…@Timwi:既然可以避免问题,为什么还要解决问题?:)让Canvas.SetLeft恢复正常是很容易的,但是如果您以简单的方式进行操作,那么当您第一次移动鼠标时,您会得到一个跳跃。我更新了我的答案,使其能够平滑居中。@Timwi:
scale.ScaleX
很好,因为我从来没有直接设置它,我只是不断地更改动画。做得好,我很乐意为您所做的大编辑第二次更新它。只是想澄清一下:我关于scale.ScaleX的意思是,如果我试图直接设置它,同样的问题也会发生。但无论如何。谢谢你的帮助,你教了我很多关于WPF的知识。
void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed)
        return;

    ellipse.CaptureMouse();

    var scaleAnimate = new DoubleAnimation(1.25,
        new Duration(TimeSpan.FromSeconds(1)));
    scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
    scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);

    // We are going to move the center of the ellipse to the mouse
    // location immediately, so start the animation with a shift to
    // get it back to the current center and end the animation at 0.  
    var offsetInEllipse = e.GetPosition(ellipse);
    translate.BeginAnimation(TranslateTransform.XProperty, 
        new DoubleAnimation(ellipse.Width / 2 - offsetInEllipse.X, 0, 
            new Duration(TimeSpan.FromSeconds(1))));
    translate.BeginAnimation(TranslateTransform.YProperty, 
        new DoubleAnimation(ellipse.Height / 2 - offsetInEllipse.Y, 0, 
            new Duration(TimeSpan.FromSeconds(1))));

    MoveEllipse(e);
}

void ellipse_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
        return;

    MoveEllipse(e);
}

private void MoveEllipse(MouseEventArgs e)
{
    var pos = e.GetPosition(canvas);
    Canvas.SetLeft(ellipse, pos.X - ellipse.Width / 2);
    Canvas.SetTop(ellipse, pos.Y - ellipse.Height / 2);
}
<Canvas Name="canvas">
    <Ellipse Name="ellipse" Width="100" Height="100" Stroke="Black" StrokeThickness="3" Fill="GreenYellow"
             RenderTransformOrigin="0.5,0.5">
        <Ellipse.RenderTransform>
            <TransformGroup>
                <ScaleTransform />
                <TranslateTransform />
            </TransformGroup>                
        </Ellipse.RenderTransform>
    </Ellipse>
</Canvas>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Canvas.SetLeft(ellipse, 0);
        Canvas.SetTop(ellipse, 0);

        ellipse.MouseDown += new MouseButtonEventHandler(ellipse_MouseDown);
        ellipse.MouseMove += new MouseEventHandler(ellipse_MouseMove);
        ellipse.MouseUp += new MouseButtonEventHandler(ellipse_MouseUp);
    }

    private ScaleTransform EllipseScaleTransform
    {
        get { return (ScaleTransform)((TransformGroup)ellipse.RenderTransform).Children[0]; }
    }

    private TranslateTransform EllipseTranslateTransform
    {
        get { return (TranslateTransform)((TransformGroup)ellipse.RenderTransform).Children[1]; }
    }

    void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed)
            return;

        ellipse.CaptureMouse();
        var pos = e.GetPosition(canvas);

        AnimateScaleTo(1.25);
    }

    void ellipse_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
            return;

        var pos = e.GetPosition(canvas);

        AnimatePositionTo(pos);
    }

    void ellipse_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (!ellipse.IsMouseCaptured)
            return;
        ellipse.ReleaseMouseCapture();

        AnimateScaleTo(1);
    }

    private void AnimateScaleTo(double scale)
    {
        var animationDuration = TimeSpan.FromSeconds(1);
        var scaleAnimate = new DoubleAnimation(scale, new Duration(animationDuration));
        EllipseScaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
        EllipseScaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);
    }

    private void AnimatePositionTo(Point pos)
    {
        var xOffset = pos.X - ellipse.Width * 0.5;
        var yOffset = pos.Y - ellipse.Height * 0.5;

        var animationDuration = TimeSpan.FromSeconds(1);

        EllipseTranslateTransform.BeginAnimation(TranslateTransform.XProperty,
            new DoubleAnimation(xOffset, new Duration(animationDuration)));

        EllipseTranslateTransform.BeginAnimation(TranslateTransform.YProperty,
            new DoubleAnimation(yOffset, new Duration(animationDuration)));
    }
}