为什么我的WPF平移动画会在完成之前停止?

6

我编写了一个WindowExtension,旨在为窗口提供简单的翻译动画。但是这个动画总是在到达目标坐标之前停止。有人能给我建议吗?

最好的问候, 克里斯

   public static class WindowExtensions
   {
      public static void Translate(this Window element, double x, double y, TimeSpan duration)
      {
         NameScope.SetNameScope(element, new NameScope());

         var xAnimation = new DoubleAnimationUsingKeyFrames {Duration = duration};
         xAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(element.Left, KeyTime.FromPercent(0)));
         xAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(x, KeyTime.FromPercent(1)));

         var yAnimation = new DoubleAnimationUsingKeyFrames {Duration = duration};
         yAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(element.Top, KeyTime.FromPercent(0)));
         yAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(y, KeyTime.FromPercent(1)));

         var storyboard = new Storyboard()
         {
            Children = { xAnimation, yAnimation }
         };

         Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Window.Left)"));
         Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Window.Top)"));

         storyboard.Duration = duration;
         storyboard.FillBehavior = FillBehavior.Stop;

         storyboard.Completed += (sender, args) =>
         {
            storyboard.SkipToFill();
            storyboard.Remove(element);
         };

         storyboard.Begin(element);
      }
   }

这可以在 WPF 窗口中进行简单测试,如下所示:

   public partial class MainWindow : Window
   {
      public MainWindow()
      {
         InitializeComponent();
      }

      private void Button_Click(object sender, RoutedEventArgs e)
      {
         this.Translate(10,10, TimeSpan.FromMilliseconds(250));
      }
   }
3个回答

4

WPF窗口的定位/重新调整大小,以及其DPI独立缩放,一直是我的难点(特别是当您想要平滑地动画移动/大小更改时,要尊重监视器DPI和多监视器设置)。

我编写了一个自定义辅助类来帮助动画窗口尺寸,这也可能对您有所帮助。

主要类(NativeWindowSizeManager):

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

/// <summary>
/// C Enumerator to Represent Special Window Handles
/// </summary>
public enum SpecialWindowHandles {
  kHwndTop = 0,
  kHwndBottom = 1,
  kHwndTopmost = -1,
  kHwndNotopmost = -2
}

/// <summary>
/// C Enumerator to Set Window Position Flags
/// </summary>
public enum SetNativeWindowPosition {
  kNoSize = 0x0001,
  kNoMove = 0x0002,
  kNoZOrder = 0x0004,
  kNoRedraw = 0x0008,
  kNoActivate = 0x0010,
  kDrawFrame = 0x0020,
  kFrameChanged = 0x0020,
  kShowWindow = 0x0040,
  kHideWindow = 0x0080,
  kNoCopyBits = 0x0100,
  kNoOwnerZOrder = 0x0200,
  kNoReposition = 0x0200,
  kNoSendChanging = 0x0400,
  kDeferErase = 0x2000,
  kAsyncWindowPos = 0x4000
}

/// <summary>
/// Class to perform Window Resize Animations
/// </summary>
public class NativeWindowSizeManager {
  #region Member Variables
  /// <summary>
  /// Attached Dependency Property for Native Window Height
  /// </summary>
  public static readonly
    DependencyProperty NativeWindowHeightProperty = DependencyProperty.RegisterAttached(
      "NativeWindowHeight",
      typeof(double),
      typeof(Window),
      new PropertyMetadata(OnNativeDimensionChanged));

  /// <summary>
  /// Attached Dependency Property for Native Window Width
  /// </summary>
  public static readonly
    DependencyProperty NativeWindowWidthProperty = DependencyProperty.RegisterAttached(
      "NativeWindowWidth",
      typeof(double),
      typeof(Window),
      new PropertyMetadata(OnNativeDimensionChanged));

  /// <summary>
  /// Attached Dependency Property for Native Window Left
  /// </summary>
  public static readonly
    DependencyProperty NativeWindowLeftProperty = DependencyProperty.RegisterAttached(
      "NativeWindowLeft",
      typeof(double),
      typeof(Window),
      new PropertyMetadata(OnNativeDimensionChanged));

  /// <summary>
  /// Attached Dependency Property for Native Window Top
  /// </summary>
  public static readonly
    DependencyProperty NativeWindowTopProperty = DependencyProperty.RegisterAttached(
      "NativeWindowTop",
      typeof(double),
      typeof(Window),
      new PropertyMetadata(OnNativeDimensionChanged));

  /// <summary>
  /// Private member holding Dpi Factor
  /// </summary>
  private static double? _dpiFactor;
  #endregion

  #region Constructors
  #endregion

  #region Commands & Properties
  #endregion

  #region Methods
  /// <summary>
  /// Sets the native height.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <param name="value">The value.</param>
  public static void SetNativeWindowHeight(UIElement element, double value) {
    element.SetValue(NativeWindowHeightProperty, value);
  }

  /// <summary>
  /// Gets the native height.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <returns>Native Height in pixels</returns>
  public static double GetNativeWindowHeight(UIElement element) {
    return (double)element.GetValue(NativeWindowHeightProperty);
  }

  /// <summary>
  /// Sets the native width.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <param name="value">The value.</param>
  public static void SetNativeWindowWidth(UIElement element, double value) {
    element.SetValue(NativeWindowWidthProperty, value);
  }

  /// <summary>
  /// Gets the native width.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <returns>Native Width in pixels</returns>
  public static double GetNativeWindowWidth(UIElement element) {
    return (double)element.GetValue(NativeWindowWidthProperty);
  }

  /// <summary>
  /// Sets the native left.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <param name="value">The value.</param>
  public static void SetNativeWindowLeft(UIElement element, double value) {
    element.SetValue(NativeWindowLeftProperty, value);
  }

  /// <summary>
  /// Gets the native left.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <returns>Native Left in pixels</returns>
  public static double GetNativeWindowLeft(UIElement element) {
    return (double)element.GetValue(NativeWindowLeftProperty);
  }

  /// <summary>
  /// Sets the native top.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <param name="value">The value.</param>
  public static void SetNativeWindowTop(UIElement element, double value) {
    element.SetValue(NativeWindowTopProperty, value);
  }

  /// <summary>
  /// Gets the native top.
  /// </summary>
  /// <param name="element">The element.</param>
  /// <returns>Native Top in pixels</returns>
  public static double GetNativeWindowTop(UIElement element) {
    return (double)element.GetValue(NativeWindowTopProperty);
  }

  /// <summary>
  /// Method to Get Dpi Factor
  /// </summary>
  /// <param name="window">Window Object</param>
  /// <returns>Dpi Factor</returns>
  public static double GetDpiFactor(Visual window) {
    HwndSource windowHandleSource = PresentationSource.FromVisual(window) as HwndSource;
    if (windowHandleSource != null && windowHandleSource.CompositionTarget != null) {
      Matrix screenmatrix = windowHandleSource.CompositionTarget.TransformToDevice;
      return screenmatrix.M11;
    }

    return 1;
  }

  /// <summary>
  /// Method to Retrieve Dpi Factor for Window
  /// </summary>
  /// <param name="window">Requesting Window</param>
  /// <param name="originalValue">Dpi Independent Unit</param>
  /// <returns>Pixel Value</returns>
  private static int ConvertToDpiDependentPixels(Visual window, double originalValue) {
    if (_dpiFactor == null) {
      _dpiFactor = GetDpiFactor(window);
    }

    return (int)(originalValue * _dpiFactor);
  }

  /// <summary>
  /// Handler For all Attached Native Dimension property Changes
  /// </summary>
  /// <param name="obj">Dependency Object</param>
  /// <param name="e">Property Arguments</param>
  private static void OnNativeDimensionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {
    var window = obj as Window;
    if (window == null)
      return;

    IntPtr handle = new WindowInteropHelper(window).Handle;
    var rect = new Rect();
    if (!GetWindowRect(handle, ref rect))
      return;

    rect.X = ConvertToDpiDependentPixels(window, window.Left);
    rect.Y = ConvertToDpiDependentPixels(window, window.Top);
    rect.Width = ConvertToDpiDependentPixels(window, window.ActualWidth);
    rect.Height = ConvertToDpiDependentPixels(window, window.ActualHeight);

    if (e.Property == NativeWindowHeightProperty) {
      rect.Height = ConvertToDpiDependentPixels(window, (double)e.NewValue);
    } else if (e.Property == NativeWindowWidthProperty) {
      rect.Width = ConvertToDpiDependentPixels(window, (double)e.NewValue);
    } else if (e.Property == NativeWindowLeftProperty) {
      rect.X = ConvertToDpiDependentPixels(window, (double)e.NewValue);
    } else if (e.Property == NativeWindowTopProperty) {
      rect.Y = ConvertToDpiDependentPixels(window, (double)e.NewValue);
    }

    SetWindowPos(
      handle,
      new IntPtr((int)SpecialWindowHandles.kHwndTop),
      rect.X,
      rect.Y,
      rect.Width,
      rect.Height,
      (uint)SetNativeWindowPosition.kShowWindow);
  }
  #endregion

  #region Native Helpers
  [DllImport("user32.dll", SetLastError = true)]
  private static extern bool GetWindowRect(IntPtr windowHandle, ref Rect rect);

  [DllImport("user32.dll")]
  [return: MarshalAs(UnmanagedType.Bool)]
  private static extern bool SetWindowPos(
    IntPtr windowHandle, IntPtr windowHandleInsertAfter, int x, int y, int cx, int cy, uint windowPositionFlag);

  /// <summary>
  /// C Structure To Represent Window Rectangle
  /// </summary>
  [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented",
    Justification = "This is an Implementation for C Struct")]
  [StructLayout(LayoutKind.Sequential)]
  public struct Rect {
    public int X;
    public int Y;
    public int Width;
    public int Height;
  }
  #endregion
}

现在,在您的Button.Click处理程序中,您可以有如下内容:

private void ButtonBase_OnClick(object sender, RoutedEventArgs e) {
  var storyBoard = new Storyboard { Duration = new Duration(new TimeSpan(0, 0, 0, 0, 250)) };

  // Top
  var aniTop = new DoubleAnimationUsingKeyFrames { Duration = new Duration(new TimeSpan(0, 0, 0, 0, 250)) };
  aniTop.KeyFrames.Add(new EasingDoubleKeyFrame(Top, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 00))));
  aniTop.KeyFrames.Add(new EasingDoubleKeyFrame(10, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 250))));
  Storyboard.SetTarget(aniTop, this);
  Storyboard.SetTargetProperty(aniTop, new PropertyPath(NativeWindowSizeManager.NativeWindowTopProperty));
  storyBoard.Children.Add(aniTop);

  // Left
  var aniLeft = new DoubleAnimationUsingKeyFrames { Duration = new Duration(new TimeSpan(0, 0, 0, 0, 250)) };
  aniLeft.KeyFrames.Add(new EasingDoubleKeyFrame(Left, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 00))));
  aniLeft.KeyFrames.Add(new EasingDoubleKeyFrame(10, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 250))));
  Storyboard.SetTarget(aniLeft, this);
  Storyboard.SetTargetProperty(aniLeft, new PropertyPath(NativeWindowSizeManager.NativeWindowLeftProperty));
  storyBoard.Children.Add(aniLeft);
  storyBoard.Begin();
}

并且在所有上述情况下都应该正常工作。 NativeWindowSizeManager 还具有 NativeWindowWidthNativeWindowHeight,允许动画调整大小或像我的情况一样,在居中于当前窗口屏幕的同时动画调整窗口大小。
您可以获取此项目的演示版以供您使用:此处

1

看起来是个打字错误。很可能是因为您的 xAnimation 动画化了 Window.Top,而 yAnimation 动画化了 Window.Left

Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Window.Left)"));
Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Window.Top)"));

哦!那是真的,那是一个打字错误...我纠正了我的错误,但我仍然得到相同的结果 :( - The Chris
1
嗯,非常奇怪,在我的情况下不一致。我尝试删除“Completed”事件,改为“DoubleAnimation”。甚至在XAML中编写了具有固定“To”值的相同动画,有时它会到达目标点,有时则不会。 - dkozl
首先要感谢您测试这个问题。我也尝试在CompletedEventHandler中递归调用Translate方法。(当然只是为了测试目的)在那里,我发现了一些有趣(奇怪)的事情。我添加了一个检查目标坐标和窗口实际左侧和顶部值相等的条件。它们是相等的,但窗口并没有位于该位置。 - The Chris

-1

我找到了一个满足我的需求的东西,但它很糟糕,我想用更好的实现方式来改变它。所以,如果有人知道原因...请告诉我 :)

  public static void Translate(this Window element, double x, double y, TimeSpan duration)
  {
     var xAnimation = new DoubleAnimationUsingKeyFrames { Duration = duration };
     xAnimation.KeyFrames.Add(new LinearDoubleKeyFrame(element.Left, KeyTime.FromPercent(0.0)));
     xAnimation.KeyFrames.Add(new LinearDoubleKeyFrame(x, KeyTime.FromPercent(1.0)));

     var yAnimation = new DoubleAnimationUsingKeyFrames { Duration = duration };
     yAnimation.KeyFrames.Add(new LinearDoubleKeyFrame(element.Top, KeyTime.FromPercent(0.0)));
     yAnimation.KeyFrames.Add(new LinearDoubleKeyFrame(y, KeyTime.FromPercent(1.0)));

     Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Window.Left)"));
     Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Window.Top)"));

     var storyboard = new Storyboard
     {
        Children = { yAnimation, xAnimation },
        Duration = duration,
        FillBehavior = FillBehavior.Stop,
     };

     storyboard.Completed += (sender, args) =>
     {
        storyboard.SkipToFill();
        storyboard.Remove(element);

        element.InvalidateProperty(Window.LeftProperty);
        element.InvalidateProperty(Window.TopProperty);

        if (Math.Abs(element.Left - x) > Double.Epsilon || Math.Abs(element.Top - y) > Double.Epsilon)
           Translate(element, x, y, TimeSpan.FromTicks(Math.Min(duration.Ticks / 2, 100)));
     };

     element.Dispatcher.Invoke(DispatcherPriority.ApplicationIdle, new Action(() => element.BeginStoryboard(storyboard)));
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接