If you have ever tried animating the Width / Height of a Window control, you would notice some jerkiness during the animation. This happens because the Window control is not really living entirely in the WPF sandbox. It is part WPF and part Native. To achieve a really smooth animation, you have GOT TO BE inside the WPF sandbox. But how do I do it?
The AnimationWindow
Creating animations directly on the Window is definitely not a desired solution. You have to first push the window contents (both the client-area and the non-client-area) into the WPF sandbox. You can do this by deriving from the GlassWindow control. You can create very customized ControlTemplates and replicate the look of a native window. You also have the flexibility of completely changing the look and feel of your application windows. This is a first step towards moving the Window content into the WPF sandbox.
However you still have the same problem when it comes to animating the window. That is because the GlassWindow is still using the native window wrapper and animating its Width / Height would still cause the jerkiness. What we need to do is push the entire window contents into a WPF control and then animate that control.
This is exactly what the AnimationWindow does. It derives from GlassWindow and overrides its OnApplyTemplate() method. Inside the override it caches a reference to the top level container that contains the complete window contents (client + non-client). When you trigger resizing animations on the Window, the actual animation happens on this container. Since you are completely inside the WPF sandbox, the animations are smooth and your customers happy ;-).
So what’s the Trick ?
[1] Derive from GlassWindow and override OnApplyTemplate()
[TemplatePart(Type = typeof(Grid), Name = "PART_WindowContent")]
public class AnimationWindow : GlassWindow
{
private Grid _contentGrid;
private Storyboard _resizeAnimator;
public static readonly DependencyProperty NewSizeProperty = DependencyProperty.Register(
"NewSize", typeof(Size), typeof(AnimationWindow), new PropertyMetadata(new Size()));
public Size NewSize
{
get { return (Size)GetValue(NewSizeProperty); }
set { SetValue(NewSizeProperty, value); }
}
static AnimationWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(AnimationWindow),
new FrameworkPropertyMetadata(typeof(AnimationWindow)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Get a reference to the ContentGrid
_contentGrid = GetChildControl & lt; Grid & gt; ("PART_WindowContent");
}
}
Before starting the animation you need to set the NewSize DependencyProperty. The GetChildControl<T> is a protected method on GlassWindow. Inside OnApplyTemplate() I am caching the top-level container as _contentGrid.
[2] Expose a method to animate window
public void ApplyResizeAnimation()
{
Size currentSize = new Size(Width, Height);
// Temporarily resize the ContentGrid to avoid automatic layout flashes
_contentGrid.SetValue(WidthProperty, currentSize.Width);
_contentGrid.SetValue(HeightProperty, currentSize.Height);
if (NewSize.Width > currentSize.Width)
{
Width = NewSize.Width;
}
if (NewSize.Height > currentSize.Height)
{
Height = NewSize.Height;
}
PlayAnimation(currentSize);
}
private void PlayAnimation(Size currentSize)
{
// Create Storyboard
_resizeAnimator = PrepareStoryboard(currentSize);
_resizeAnimator.CurrentStateInvalidated += ResizeAnimator_CurrentStateInvalidated;
_resizeAnimator.Begin(_contentGrid, true);
}
void ResizeAnimator_CurrentStateInvalidated(object sender, EventArgs e)
{
Clock clock = sender as Clock;
if (clock.CurrentState != ClockState.Active)
{
Width = NewSize.Width;
Height = NewSize.Height;
// Clear Width/Height settings and switch to automatic layout
_contentGrid.ClearValue(WidthProperty);
_contentGrid.ClearValue(HeightProperty);
_resizeAnimator.CurrentStateInvalidated -= ResizeAnimator_CurrentStateInvalidated;
_resizeAnimator.Remove(_contentGrid);
}
}
private Storyboard PrepareStoryboard(Size size)
{
Storyboard board = new Storyboard();
board.FillBehavior = FillBehavior.HoldEnd;
// Width
DoubleAnimation wAnim = new DoubleAnimation(size.Width, NewSize.Width,
new Duration(TimeSpan.FromMilliseconds(500)));
Storyboard.SetTargetProperty(wAnim, new PropertyPath("(0)", WidthProperty));
// Height
DoubleAnimation hAnim = new DoubleAnimation(size.Height, NewSize.Height,
new Duration(TimeSpan.FromMilliseconds(500)));
Storyboard.SetTargetProperty(hAnim, new PropertyPath("(0)", HeightProperty));
board.Children.Add(wAnim);
board.Children.Add(hAnim);
return board;
}
In ApplyResizeAnimation() I first explicitly set the Width + Height of the _contentGrid. I do this to avoid some automatic layout flashes. These would happen if my NewSize is greater than the current size. I then clear the explicit Width + Height settings when the animation gets over. Note that I am exploiting the precedence nature of the DependencyProperty.
[3] Create a ControlTemplate for the Window with transparency
<Style x:Key="TestWindowStyle"
TargetType="{x:Type Controls:AnimationWindow}">
<Setter Property="AllowsTransparency"
Value="True"/>
<Setter Property="WindowStyle"
Value="None"/>
<Setter Property="ResizeMode"
Value="CanResize"/>
<Setter Property="Template"
Value="{StaticResource TestWindow_Template}"/>
</Style>
With all of these, your animations should run super smooth!
Getting Creative
Note that you can pull off some really exotic tricks with the AnimationWindow. Right now the resize animations are hard-coded. You can change that to expose a property on AnimationWindow to set custom Storyboards. If you club it with the TransitionContainer, you can even have Mac OSX style genie effects, right on the Desktop !!