使用WPF动画窗口的宽度和高度

5
我想要对一个WPF窗口的宽度和高度进行动画处理。我已经尝试了下面的代码,但是很遗憾只有宽度会被动画处理,而窗口的高度没有变化。我相信我错过了一些细节,希望在这里发布后能够有人看到我的错误!下面是一个简单窗口的代码,其中包含一个按钮,我已经将其连接到了调整大小的功能:
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.AnimateWindowSize(ActualWidth + 200, ActualHeight + 200);
    }
}

这里是我编写的动画代码,作为扩展方法,可以应用于任何窗口...

public static class WindowUtilties
{
    public static void AnimateWindowSize(this Window target, double newWidth, double newHeight)
    {
        var sb = new Storyboard {Duration = new Duration(new TimeSpan(0, 0, 0, 0, 200))};

        var aniWidth = new DoubleAnimationUsingKeyFrames();
        var aniHeight = new DoubleAnimationUsingKeyFrames();

        aniWidth.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 200));
        aniHeight.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 200));

        aniHeight.KeyFrames.Add(new EasingDoubleKeyFrame(target.ActualHeight, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 00))));
        aniHeight.KeyFrames.Add(new EasingDoubleKeyFrame(newHeight, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 200))));
        aniWidth.KeyFrames.Add(new EasingDoubleKeyFrame(target.ActualWidth, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 00))));
        aniWidth.KeyFrames.Add(new EasingDoubleKeyFrame(newWidth, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 200))));

        Storyboard.SetTarget(aniWidth, target);
        Storyboard.SetTargetProperty(aniWidth, new PropertyPath(Window.WidthProperty));

        Storyboard.SetTarget(aniHeight, target);
        Storyboard.SetTargetProperty(aniHeight, new PropertyPath(Window.HeightProperty));

        sb.Children.Add(aniWidth);
        sb.Children.Add(aniHeight);

        sb.Begin();
    }
}

提前感谢任何帮助。

3个回答

2

在Joe提出使用pinvoke和依赖属性的建议后,我最终得到了这段代码。如果代码很长并且我不应该把它都放在这里,请先道歉。尺寸上的计算并不完美。WPF实际(高度/宽度)与Rect.Height/Width之间存在相当大的差异,可能需要进行一些计算才能获得精确的尺寸。

这个代码段被添加到MainWindow类中。

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int X;
    public int Y;
    public int Width;
    public int Height;
}

public enum SpecialWindowHandles
{
    HWND_TOP = 0,
    HWND_BOTTOM = 1,
    HWND_TOPMOST = -1,
    HWND_NOTOPMOST = -2
}

[DllImport("user32.dll", SetLastError = true)]
static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

public static readonly DependencyProperty WindowHeightAnimationProperty = DependencyProperty.Register("WindowHeightAnimation", typeof(double),
                                                                                            typeof(MainWindow), new PropertyMetadata(OnWindowHeightAnimationChanged));

private static void OnWindowHeightAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;

    if (window != null)
    {
        IntPtr handle = new WindowInteropHelper(window).Handle;
        var rect = new RECT();
        if (GetWindowRect(handle, ref rect))
        {
            rect.X = (int)window.Left;
            rect.Y = (int)window.Top;

            rect.Width = (int)window.ActualWidth;
            rect.Height = (int)(double)e.NewValue;  // double casting from object to double to int

            SetWindowPos(handle, new IntPtr((int)SpecialWindowHandles.HWND_TOP), rect.X, rect.Y, rect.Width, rect.Height, (uint)SWP.SHOWWINDOW);
        }
    }
}

public double WindowHeightAnimation
{
    get { return (double)GetValue(WindowHeightAnimationProperty); }
    set { SetValue(WindowHeightAnimationProperty, value); }
}

public static readonly DependencyProperty WindowWidthAnimationProperty = DependencyProperty.Register("WindowWidthAnimation", typeof(double),
                                                                                            typeof(MainWindow), new PropertyMetadata(OnWindowWidthAnimationChanged));

private static void OnWindowWidthAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;

    if (window != null)
    {
        IntPtr handle = new WindowInteropHelper(window).Handle;
        var rect = new RECT();
        if (GetWindowRect(handle, ref rect))
        {
            rect.X = (int)window.Left;
            rect.Y = (int) window.Top;
            var width = (int)(double)e.NewValue;
            rect.Width = width;
            rect.Height = (int) window.ActualHeight;

            SetWindowPos(handle, new IntPtr((int)SpecialWindowHandles.HWND_TOP), rect.X, rect.Y, rect.Width, rect.Height, (uint)SWP.SHOWWINDOW);
        }
    }
}

public double WindowWidthAnimation
{
    get { return (double)GetValue(WindowWidthAnimationProperty); }
    set { SetValue(WindowWidthAnimationProperty, value); }
}

private void GrowClick(object sender, RoutedEventArgs e)
{
    this.AnimateWindowSize(Width+200, Height+200);
}

/// <summary>
/// SetWindowPos Flags
/// </summary>
public static class SWP
{
    public static readonly int
    NOSIZE = 0x0001,
    NOMOVE = 0x0002,
    NOZORDER = 0x0004,
    NOREDRAW = 0x0008,
    NOACTIVATE = 0x0010,
    DRAWFRAME = 0x0020,
    FRAMECHANGED = 0x0020,
    SHOWWINDOW = 0x0040,
    HIDEWINDOW = 0x0080,
    NOCOPYBITS = 0x0100,
    NOOWNERZORDER = 0x0200,
    NOREPOSITION = 0x0200,
    NOSENDCHANGING = 0x0400,
    DEFERERASE = 0x2000,
    ASYNCWINDOWPOS = 0x4000;
}

在OP的代码中,我相应地更改了高度和宽度目标属性。

Storyboard.SetTargetProperty(aniHeight, new PropertyPath(Window.HeightProperty));
Storyboard.SetTargetProperty(aniWidth, new PropertyPath(Window.WidthProperty));

to

Storyboard.SetTargetProperty(aniHeight, new PropertyPath(MainWindow.WindowHeightAnimationProperty));
Storyboard.SetTargetProperty(aniWidth, new PropertyPath(MainWindow.WindowWidthAnimationProperty));

原始答案:

根据我的发现,您的代码没有问题。当我更改了将动画添加到故事板(sb.Children.Add)实例的顺序时,我得到了高度动画而没有宽度。

这让我相信,当第一个动画发生时,其他动画变得无效。

我能想到的唯一办法是让它们一个接一个地动画,通过使一个动画比另一个动画略长。较长的动画将在第一个动画完成后发生。

var sb = new Storyboard { Duration = new Duration(new TimeSpan(0, 0, 0, 0, 300)) };

var aniWidth = new DoubleAnimationUsingKeyFrames();
var aniHeight = new DoubleAnimationUsingKeyFrames();

aniWidth.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 300));
aniHeight.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 150));

aniHeight.KeyFrames.Add(new EasingDoubleKeyFrame(target.ActualHeight, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 00))));
aniHeight.KeyFrames.Add(new EasingDoubleKeyFrame(newHeight, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 150))));

aniWidth.KeyFrames.Add(new EasingDoubleKeyFrame(target.ActualWidth, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 150))));
aniWidth.KeyFrames.Add(new EasingDoubleKeyFrame(newWidth, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 0, 300))));

即使使用 XAML storyboards,我也无法同时调整窗口的高度和宽度。


这也是我发现的...我想知道是否有使用平行时间轴的解决方案,但我也无法使其工作。这个https://dev59.com/W0rSa4cB1Zd3GeqPWGvf?rq=1看起来像是一个hack,但也许这是唯一的方法让它工作? - Cameron Peters
是的,看起来可能需要手动完成。那个链接很不错。 - Shane Charles
1
Window 上的一些顶级属性有点奇怪,并且与其他 WPF 属性的行为方式不同,因为它们处于 Win32 的边界上。如果您想以 WPF 的方式将它们一起动画化,一种方法是在 Window 上创建一个附加 DependencyProperty,在其背后的 HwndSource 上调用 SetWindowPos。我没有可用的 Windows 开发环境来编写解决方案,所以将其作为评论而不是答案。如果您在使用 WPF pre-4.0,则需要担心其他问题。如果有机会,我将编写一个解决方案。 - Joe Castro
@Joe 谢谢。在阅读了您的评论后,我开始尝试一下,使用WPF动画来调整窗口大小效果不错。 - Shane Charles
1
很高兴你解决了它 :) 数学有点奇怪的原因可能是因为你的宽度和高度包括窗口的边框。使用p/invoking GetClientRect可能会有所帮助。此外,Win32函数以像素而不是设备独立像素(dips)工作,在96dpi下相同,但通常需要小心处理高DPI并进行转换。我建议的另一个更改是DependencyProperty.RegisterAttached(),使其与窗口相关联。这也将使其符合XAML规范。 - Joe Castro

1

我知道这是一个旧的线程,但我找到了一个似乎“可接受”的解决方法,至少在.NET 7中经过测试。不要动画化窗口属性,而是设置内容大小并使用SizeToContent="WidthAndHeight"定义窗口大小。然后动画显示网格大小。肯定不能很平滑(因为每帧都会导致重绘,因为这正是窗口的工作方式,我怀疑任何解决方案都无法解决),但至少宽度/高度同时运行。


<Window ... SizeToContent="WidthAndHeight">
    <Window.Resources>
        <Storyboard x:Key="MainStory">
            <DoubleAnimation Storyboard.TargetName="SizingGrid" Storyboard.TargetProperty="(Grid.Width)" Duration="00:00:01" To="1200"/>
            <DoubleAnimation Storyboard.TargetName="SizingGrid" Storyboard.TargetProperty="(Grid.Height)" Duration="00:00:01" To="800"/>
        </Storyboard>
    </Window.Resources>
    <Grid Name="SizingGrid" Width="640" Height="480">
        ...
    </Grid>
</Window>

0

对于我来说,新的 DP 建议似乎有点过度,特别是因为解决方案承认它仍然无法同时调整大小。在我的快速实验中,添加一个延迟(通过 Task.Run())即使只有 1 毫秒也可以实现最终结果(窗口调整大小)。这个解决方案也不会同时调整大小,所以动画不太优雅,但最终“可行”。


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