Share via


WPF: Seasons Greetings

This article explains how a greeting card style WPF application works.


Introduction

The sample which goes along with this partly exists just as a bit of fun. Even an animated greetings card has little direct use beyond warming the hearts of people looking at it. You can download the sample from the TechNet Gallery.
There are a number of techniques used in the sample which WPF developers might find useful in rather more serious applications. Flashy animating adverts or notifications spring to mind.
It also illustrates some problem areas a developer may want to avoid.

Behold

The application looks like this running:

Or at least it does if you take a snap shot and then scrunch it down. The real thing is much more animated than the static picture conveys.
 
The greeting message animates in repeatedly. It loops through a list of greetings in various languages. The decorations on the tree all flash, including the star on top. There are three little twinkling stars which also flash. They're a bit difficult to make out in the above picture. 

The text of the greeting is made a bit prettier by using a custom control. 

Enough With the Fun

OK, you watched it flashing and thrilled to the greetings animating in. You want to know about how the thing is put together.

The Holly Border

This uses a free png file.  The original had a white background and is taller than it is wide. The picture was trimmed down, reduced in size and the background flood filled with a slightly orange red in MS Paint.
This is the result:

Red is a complimentary colour to green so it offsets the holly leaves and making it slightly orange then also gives some complimentary contrast with the blue fill in the "sky".

That png is then tiled to form the Border.

<Grid>
    <Grid.Resources>
        <ImageBrush x:Key="Holly4Path" ImageSource="Holly.png" Viewport="0,0,20,20" ViewportUnits="Absolute" TileMode="Tile" />
    </Grid.Resources>
    <Border
        BorderBrush="{StaticResource Holly4Path}"
        Grid.RowSpan="2" Margin="20" BorderThickness="22" CornerRadius="40">
        <Border.Background>
            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <GradientStop Color="DarkBlue" Offset="0" />
                <GradientStop Color="Blue" Offset="0.2" />
                <GradientStop Color="White" Offset="1.0" />
            </LinearGradientBrush>
        </Border.Background>

That combination of ViewPort size units Absolute and TileMode Tile gives you a tiled pattern of 20x20 holly pictures.
There is a bit more to this than is immediately obvious. The way it works is like it fills the entire window with these tiles but you only see those which fall within the border area. If you grab the side of the window and drag it slightly this will be immediately obvious. You can see the next row of holly pictures appear as you drag.
That looks quite odd when you see it the first time,
There is technically a way to draw with a pen that uses a picture repeatedly but that then would make the rounded rectangular shape harder.

That gradient you see behind everything is the other noticeable part of the markup. The border background is a LinearGradientBrush which is "horizontal" in that the stops are arranged horizontally. 3 stops are used in order to make the top rather darker. This in turn adds a bit more interest to the scene, giving a bit of an illusion of distance in there and increasing the contrast for the tiny stars in the distance and the big one on top of the tree.

Greeting Text

The text itself is outlined using the OutLineTextControl which is based on this Text as a Decorative Graphic. You can read more about the mechanics of how that works in that link. Using that control allows us to have a brush for the outline as well as a brush for the text. It's outlining the text which is tricky without using that control. You could use dropshadow but that will make the text a bitmap which means it'd lose the crispness you see when this thing is running.

Technically, you could put white "snow" sort of on top of the tree, presents and the text. That isn't quite so effective on the tree without a lot of work because the brush would apply to the entire tree's outline. Maybe we'll do that later.

The greeting goes on the left of the tree because it can be a bigger font and still fit than if it was the tree left with the greeting right. Some of the phrases are fairly long.

Here's the markup for the text.

<local:OutlineTextControl HorizontalAlignment="Left"
                      Grid.ColumnSpan="2"
                      Grid.Row="1" Font="Forte"
                      Stroke="Black"
                      StrokeThickness="2"
                      Fill="{StaticResource OrangeGradient}"
                      FontSize="50"
                      Margin="20"
                      Text="{Binding Greeting, NotifyOnTargetUpdated=True}">
    <local:OutlineTextControl.RenderTransform>
        <TransformGroup>
            <TransformGroup.Children>
                <TransformCollection>
                    <RotateTransform x:Name="rotate"
                                     CenterX="0" CenterY="0" Angle="0" />
                    <ScaleTransform  x:Name="grow"
                                     CenterX="0" CenterY="0"
                                     ScaleX="1" ScaleY="1" />
                </TransformCollection>
            </TransformGroup.Children>
        </TransformGroup>
    </local:OutlineTextControl.RenderTransform>
    <local:OutlineTextControl.Triggers>
        <EventTrigger RoutedEvent="Binding.TargetUpdated">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="rotate" Storyboard.TargetProperty="Angle" From="-16" To="0" Duration="0:0:1.2" />
                    <DoubleAnimation Storyboard.TargetName="grow" Storyboard.TargetProperty="ScaleX" From="0" To="1" Duration="0:0:1.2" />
                    <DoubleAnimation Storyboard.TargetName="grow" Storyboard.TargetProperty="ScaleY" From="0" To="1" Duration="0:0:1.2" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </local:OutlineTextControl.Triggers>
</local:OutlineTextControl>

You can see that as the binding value changes, that will notify and start both animations.
The ScaleTransform makes the text grow in size in both dimensions. The RotateTransform makes it look a bit more interesting and like it sort of swoops in. That's done with a negative "From" value which starts it off rotated 16 degrees anti clockwise. Since the center X and Y are both zero that's from the left top corner.
Together you get quite a striking and eye catching effect.

The text uses a Brush which is in the Dictionary1 Resource Dictionary:

<!-- Brush for Seasons Greetings -->
<LinearGradientBrush x:Key="OrangeGradient"
      StartPoint="0,0"
      EndPoint="0,1">
    <GradientStop Color="Firebrick" Offset="0" />
    <GradientStop Color="Orange" Offset=".25" />
    <GradientStop Color="PapayaWhip"  Offset="1" />
</LinearGradientBrush>

That's made interesting by giving it a gradient, the top of which is nice and dark so it echoes the background. This is made to look a bit more eye catching by using complimentary colours. The Orange is complimentary to Blue which increase the Colour Contrast.
The black outline ( Stroke ) is Black in order to increase the Tonal contrast. If you do this sort of thing yourself you could use a complimentary colour as the outline. Dark Blue for example.

ViewModel

If you take a look at the MainWindowViewModel, you can see this is all about that greeting message.

public class MainWindowViewModel : INotifyPropertyChanged
{
    private string greeting;
 
    public string Greeting
    {
        get { return greeting; }
        set
        {
            greeting = value;
            NotifyPropertyChanged();
        }
    }
 
    private List<string> GreetingsList = new List<string>
    {
        "Season's Greetings",
        "Joyeux Noël",
        "Nollaig Shona Dhuit",
        "Feliz Natal",
        "God Jul",
        "Feliz Navidad",
        "Nadolig Llawen",
        "Froehliche Weihnachten",
        "Bada Din Mubarak Ho",
        "Hyvää Joulua",
        "Glædelig Jul",
        "Buon Natale",
        "Happy Christmas",
        "Srozhdestovm Kristovim",
        "Kala Christouyenna"
    };
 
    public MainWindowViewModel()
    {
        GreetingsLoop();
    }
 
    private async void GreetingsLoop()
    {
        while(true)
        {
            foreach (string g in GreetingsList)
            {
                Greeting = g;
                await Task.Delay(5000);
            }
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Quite a large part of that code is just the list of greetings in various languages. 
The constructore of the viewmodel starts up an async method. That while(true) means it'll loop forever through that list of greetings. It sets the Greeting property to the next string off the list. That raises property changed because the class implements INotifyPropertyChanged. The view will read that new value because bindings listen for propertychanged. That then notifies the target is updated because of that setting on the property binding. Because of that the Binding.TargetUpdated event fires and the storyboard starts animating the new piece of fancy text.
The GreetingsLoop then sort of pauses for 5 seconds due to Task.Delay. This does not block the ui thread like sleep would do so it's a convenient way of implementing a simple sort of timer.

Paths

The rest of the UI involves Path objects of one sort or another. These are defined by geometries in that Dictionary1. When you look at one of those Geometries they look like a confusing long list of numbers and the odd letter. These are a very efficient way to define scaleable shapes of some sort.
Here they are with their various definitions truncated:

<Geometry x:Key="Tree">
    M142.653,0C146.626,1E-06 ....
</Geometry>
<Geometry x:Key="Gift1">
    M18.892,28.973L32.050998.....
</Geometry>
<Geometry x:Key="Gift2">
    M327.386,279.874L327.386,.....
</Geometry>
<Geometry x:Key="Gift3">
    M33.585001,29.653999L56.838001,...
</Geometry>
<Geometry x:Key="RoundedStar">
    M383.519,0L501.986,252.479 767,....
</Geometry>
<Geometry x:Key="Star">
    M384.884,0L475.738,293.347 769....
</Geometry>
<Geometry x:Key="SnowFlake">
    M7.9500122,0L9.6599731,0 11.57....
</Geometry>

The Tree is used as the Christmas tree, the Gifts are on the left of the window, the Rounded Star goes on the tree. The star is not used but you could experiment switching that out with the SnowFlake or Rounded star in the markup and see if you prefer it that way.
The SnowFlake is used in a UserControl for the ornaments on the tree and the three little twinkly stars.

These are obtained using SyncFusion's Metro Studio ( which is free ).

Presents

The way the presents stack up allows for the window re-sizing. Other parts of the window don't handle this so well but we'll get to them later.

<Grid Grid.Row="1" Grid.Column="0" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="4">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="40" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Path Data="{StaticResource Gift1}" Height="50" Width="50"
          Fill="Red" Stretch="Fill" Grid.Row="1" />
    <Path Data="{StaticResource Gift3}" Height="50" Width="40"
          Fill="Green" Stretch="Fill" Grid.Row="1" Grid.Column="1" />
    <Path Data="{StaticResource Gift2}" Height="50" Width="40"
          Fill="Blue" Stretch="Fill" Grid.Row="0" Grid.ColumnSpan="2" />
</Grid>

This is basically a Grid with 2 rows and 2 columns. The two bottom presents go in row 1 and the top one is horizontally centralised in row 0, spanning both columns.
The whole grid is then centralised inside a cell of the main outer grid. VerticalAlignment bottom pushes it down and the Margin of 4 then makes it separate from the border. If you remove that margin you'll notice it looks a bit strange.

Tree

There isn't much to the tree, it's pretty much just a green path with dark green outline stroke.

<Canvas Grid.Row="1" Grid.Column="1"
        Margin="20,0,20,4"
        Name="TreeCanvas">
    <Path
      Name="ChristmasTree"
        Height="{Binding ElementName=TreeCanvas, Path=ActualHeight}"
        Width="{Binding ElementName=TreeCanvas, Path=ActualWidth}"
      HorizontalAlignment="Center"
      IsHitTestVisible="True"
      Stroke="DarkGreen"
      StrokeThickness="3"
      Data="{StaticResource Tree}"
      Stretch="Fill"
      Fill="Green" />

A couple of aspects of the tree are there for an experiment which will be explained later.
The Path doesn't really need to be inside that canvas as it is, the height and width binding are only necessary because it's in the Canvas. Similarly the hit testing.
The margin on the Canvas pushes it up off the border for the same reasons as the presents.
The dark green stroke emphasises the fact that this is a sort of cartoon style representation rather than photographic and increases contrast to the lighter part of the background gradient.

SnowFlake

The decorations on the tree and the three little stars are all instances of a UserControl called SnowFlake.  The markup for this makes the content and therefore pretty much the entire control a path.

    <UserControl.Resources>
        <Style TargetType="UserControl">
           <Setter Property="Height" Value="20"/>
           <Setter Property="Width" Value="20"/>
        </Style>
    </UserControl.Resources>
    <Path
         Data="{StaticResource SnowFlake}"
         Stretch="Fill"
         Fill="White"
                 >
        
        <Path.Triggers>
            <EventTrigger RoutedEvent="Path.Loaded">
                <BeginStoryboard>
                    <Storyboard >
                        <DoubleAnimation
                                        From=".2" To="1"
                                        BeginTime="{local:BeginRandom}"
                                        Duration="00:00:01"
                                        Storyboard.TargetProperty="Opacity"
                                        AutoReverse="True"
                                        RepeatBehavior="Forever">
                        </DoubleAnimation>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Path.Triggers>
    </Path>
</UserControl>

A style sets the default height and width to 20.
The opacity is animated from 0.2 to 1 in order to make these "flash". Version one had these all starting at once but that isn't terribly interesting.

BeginRandom

This is a markup extension which is used to randomise the time each Snowflake starts it's flashing from. 

public class BeginRandom : MarkupExtension
{
    public BeginRandom() { }
 
    private Random rand = new Random();
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        int milli = rand.Next(1000);
        return TimeSpan.FromMilliseconds(milli);
    }
}

The random will return a value in milliseconds from zero to 1000 ( 1 second ) which makes everything flash at slightly different times. Avoiding everything flashing together makes the decorations and stars look a bit more interesting.

Stars

Above the greeting there is a line of 3 little twinkly stars.

<Grid Width="120" HorizontalAlignment="Right" Margin="0,0,50,0">
    <Grid.RenderTransform>
        <RotateTransform CenterX="0" CenterY="20" Angle="16" />
    </Grid.RenderTransform>
    <local:SnowFlake Height="4" Width="4" HorizontalAlignment="Left" />
    <local:SnowFlake Height="3" Width="3" HorizontalAlignment="Center" />
    <local:SnowFlake Height="3" Width="3" HorizontalAlignment="Right" Margin="20" />
     
</Grid>

Since they're in a grid and aligned to both ends and centrally these would usually be horizontal. In order to liven them up a bit the grid is rotated 16 degrees clockwise. The margin on the right one means they're not all equidistant and the left one is a bit bigger as well, All to make them look more natural and add interest. It's little tricks like this can occasionally prove handy if you're doing subtle UI design that goes beyond just a textbox datagrid and button.

Tree Decorations

The snowflakes on the tree are of course more instances of that SnowFlake usercontrol positioned on the canvas the tree is within. 

<local:SnowFlake Canvas.Left="200" Canvas.Top="40" />
<local:SnowFlake Canvas.Left="260" Canvas.Top="80" />
<local:SnowFlake Canvas.Left="130" Canvas.Top="75" />
<local:SnowFlake Canvas.Left="232" Canvas.Top="156" />
<local:SnowFlake Canvas.Left="300" Canvas.Top="180" />
<local:SnowFlake Canvas.Left="260" Canvas.Top="250" />
<local:SnowFlake Canvas.Left="240" Canvas.Top="310" />
<local:SnowFlake Canvas.Left="330" Canvas.Top="280" />
<local:SnowFlake Canvas.Left="160" Canvas.Top="300" />
<local:SnowFlake Canvas.Left="100" Canvas.Top="260" />
<local:SnowFlake Canvas.Left="40" Canvas.Top="280" />
<local:SnowFlake Canvas.Left="170" Canvas.Top="112" />
<local:SnowFlake Canvas.Left="80" Canvas.Top="170" />
<local:SnowFlake Canvas.Left="182" Canvas.Top="228" />
<local:SnowFlake Canvas.Left="152" Canvas.Top="168" />

The default style based sizing is retained.
These are all hard coded and positioned by hand to avoid them looking too regular. You might think there must surely be some way of automating that and looping in code.
There is problem with that approach though.

The Problems With Coded Placement

To see this working you have to comment out the above ornaments and uncomment the "Add Decorations to Tree" button which is just underneath the OutLineTextControl. Click on that in the designer to find it and scroll down.

The code which you're going to run is in the code behind:

    private List<DependencyObject> foundControls = new List<DependencyObject>();
 
    private void  AddIfInPath(Canvas canvas, Point point, Double radius)
    { 
        var hitTestArea = new  EllipseGeometry(point, radius, radius);
        foundControls.Clear();
        VisualTreeHelper.HitTest(
              canvas
            , null
            , new  HitTestResultCallback(SelectionResult)
            , new  GeometryHitTestParameters(hitTestArea));
            foreach (var obj in foundControls)
            {
                if (obj is Path)
                {
                    if (((Path)obj).Name == "ChristmasTree")
                    {
                        var decky = new  SnowFlake();
                        Canvas.SetLeft(decky, point.X);
                        Canvas.SetTop(decky, point.Y);
                        canvas.Children.Add(decky);
                    }
                }
        }
    }
    public HitTestResultBehavior SelectionResult(HitTestResult result)
    {
        IntersectionDetail id = ((GeometryHitTestResult)result).IntersectionDetail;
        switch (id)
        {
            case IntersectionDetail.FullyContains:
            case IntersectionDetail.FullyInside:
                foundControls.Add(result.VisualHit);
                return HitTestResultBehavior.Continue;
            case IntersectionDetail.Intersects:
                foundControls.Add(result.VisualHit);
                return HitTestResultBehavior.Continue;
            default:
                return HitTestResultBehavior.Stop;
        }
    }
 
    private Random rand = new Random();
    private double  canvasWidth;
    private double  canvasHeight;
 
    private void  Button_Click(object  sender, RoutedEventArgs e)
    {
        Canvas tc = TreeCanvas;
        canvasWidth = tc.ActualWidth;
        canvasHeight = tc.ActualHeight;
        double left = 0;
        double top = 0;
        while(left < canvasWidth)
        {
            while(top < canvasHeight)
            {
                top += 50.0 + rand.Next(-9, 9);
                Point pt = new  Point(left, top);
                Debug.WriteLine("Trying " + left.ToString() + " , " + top.ToString());
                AddIfInPath(tc, pt, 10);
            }
            top = 0;
            left += 50.0 + rand.Next(-9, 9);
        }
    }
}

What that does is loop left to right on the canvas and for each of those top to bottom, incrementing x and y co-ordinates.  A bit of random wobble is added or subtracted to try and make them look less regular.
Hit testing is then carried out on the resultant point on the canvas to see whether there's the tree path is under that point.

The button click handler has the looping and randomness in it.
AddIfInPath does the fiddly hit testing and adds a control if it finds a bit of the tree. This is quite tricky code using the HitTestResultBehavior to decide not just if the point is inside the tree path but an ellipse. This is in order to try and get the decorations just on the tree.

Spin it up and hit the button and you get something like this:

As you can immediately see, there are issues. Firstly, the pseudo random placement is too regular. The big problem, though, is with the hit testing. That just does not work reliably. It's clearly sort of doing something because some areas are excluded but there are problems.
As it turns out, this sort of hit testing is unreliable with a stretched path. That particular aspect is a bit academic though since the manual placement is clearly superior, giving a much more interesting and irregular pattern.
It was also quicker to place all the ornaments manually than it was to get that code anywhere near working.

The Moral of the Tale?

Sometimes you are best designing by hand and mark 1 eyeball. If code is going to take longer than what seems a less elegant manual approach then consider whether code has any down sides. This piece of code certainly has!

The Star on Top

As well as flashing like the snowflakes, the star has a blur effect on it.

<Path Data="{StaticResource RoundedStar}"
    Stretch="Fill" Fill="White"
    Width="40" Height="40"
    HorizontalAlignment="Center" VerticalAlignment="Bottom">
    <Path.Triggers>
        <EventTrigger RoutedEvent="Path.Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation From=".6" To="1" Duration="00:00:01" Storyboard.TargetProperty="Opacity" AutoReverse="True" RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Path.Triggers>
    <Path.Effect>
        <BlurEffect KernelType="Gaussian" Radius="10" RenderingBias="Quality" />
    </Path.Effect>
</Path>

Because it's got a darker blue background and it's a greater area of white than the decorations the star stands out rather more. It also flashes faster which tends to be that bit more eye catching and emphasise it's importance. 
The star "topper" on a tree represents the Star of Bethlehem which of course is the star those three wise men followed.  The significance may well be more focal point than religious experience for many viewers but it's at the pinnacle of the tree so it's only right that it's a bit more interesting. Flashy even.

Summary

We've seen a number of techniques which could be used to jazz up UI. Clearly you are unlikely to be using all this in every business app window. On the other hand,.
Do Line of Business apps in WPF for a while and you will eventually find yourself discussing your efforts with someone who will say the dreaded words:
"That's nice. But.... Could you liven it up a bit more?"
One of those  totally vague requests which come from "someone important" who cannot just be ignored. And you are expected to dream up something creative.
Maybe some of what you've seen here could provide a starting point. At least in the form of inspiration.
 

See Also

WPF Resources on the Technet Wiki
Event handling in an MVVM WPF application