Now we need at least two worms for a race and four to make it look better. If we were to write XAML for each one, we will have same XAML repeated all over the source code, so we need some reusable code/datatype/class from which we can create various instances of a worm and also set the color of each worm. There are several ways to do this; it can be class based on frameworkelement or shape or it could be made into a usercontrol. Usercontrols are extremely popular in WPF. UserControl derives from ContentControl so the visual tree defining the control is generally defined in XAML as the control's Content property. The codebehind file might define a couple more custom properties and handles events and interactions. In other words it is an easy way of taking a collection of existing controls and bundling them together as a new resusable single control.
A Worm user control is made of a transparent border, four ellipses for a body, two small ellipses for eyes and a storyboard for animating the body. Storyboard is a way to wrap animations in XAML and allows interactive controls over animation (we can start, stop and pause animations using a storyboard). The XAML used for the Worm usercontrol is in worm.xaml.
Adding custom properties to the usercontrol
Now we need to have a property to specify the color of the worm once and it should change the fill color of all the four ellipses that make the body of the worm. So we need to add a custom property called "WormColor" to this user control. this involves three steps.
A. Registering a dependency property
The following will register a dependency property:
public static readonly DependencyProperty WormColorProperty = DependencyProperty.Register("WormColor", typeof(Brush), typeof(Worm), new
FrameworkPropertyMetadata(new PropertyChangedCallback(OnWormColorChanged)));
By convention, the DependencyProperty is named <PropertyName>Property; in this case the property name is wormcolor so the dependency property becomes "WormColorProperty". To create a new dependency property we must call the "Register" method with the following arguments:
- The Name of the property: "WormColor"
- Type of data held by the property: typeof(Brush)
- The Name of the class registering the property: typeof(Worm)
- Additional things about the property, including a callback: FrameworkPropertyMetadata(new PropertyChangedCallback(OnWormColorChanged))
Even though all we want is to register a callback that needs to be called when the property value changes, we must do it using a "FrameworkPropertyMetadata" instance only and there is no direct way of registering it.
B. Implementing dependency property
public Brush WormColor
{
get { return (Brush)GetValue(WormColorProperty); }
set { SetValue(WormColorProperty,value); }
}
The GetValue and SetValue references the dictionary of property values held by the object. The key is the dependency property we registered
(WormColorProperty) and is a combination of name and the behavior associated with the property. These properties should exist even if there are no objects and all objects of the usercontrol class should see this property so they always need to be static.
C. Implementing the callback
private static void OnWormColorChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Worm wrm = (Worm)sender;
Brush b = wrm.WormColor;
wrm.ellipse.Fill = b;
wrm.ellipse1.Fill = b;
wrm.ellipse3.Fill = b;
wrm.ellipse4.Fill = b;
}
OnWormColorChanged is the handler that is called whenever the "wormcolor" property changes, this must be static because the dependency property is static.
And the object that invoked this callback can be obtained by casting "sender" (Worm wrm = (Worm)sender). Once you have an instance, filling the ellipses to change the color of the worm is trivial.
Now we have the user control ready. We now can easily create various instances of the worm, as in:
<local:Worm x:Name="YellowWorm" WormColor="Yellow" />
<local:Worm x:Name="RedWorm" WormColor="Red" />
<local:Worm x:Name="WoodWorm" WormColor="BurlyWood"/>
<local:Worm x:Name="GreenWorm"/>
Property-based animation provides a way to directly modify the value of a dependency property over an interval of time. For example, to make a drawing of a rectangle fade out of view, you can modify its Opacity property in an animation. To make it grow or shrink, you can modify its Width and Height properties. so creating the animation you want becomes a matter of determining the properties you need to modify.
For a property to have animation capabilities, it must meet the following requirements:
- It must be a dependency property.
- It must belong to a class that inherits from DependencyObject and implements the IAnimatable interface.
- There must be a compatible animation data type available.
This means that to animate a dependency property, you need to have an animation class that supports its data type. For example, the Width property of a rectangle uses the double data type. To animate it, you use the DoubleAnimation class. However, the Color property of a SolidColorBrush object uses the color structure, so it requires the ColorAnimation class. If WPF doesn't provide the data type you want to use, you can create your own animation class for that data type.
Property-based animation and keyframe animation differ in the way they generate intermediate values of a property during a selected time interval. Property-based animation takes "From" and "To" values and uses linear interpolation to generate intermediate values and is smooth. On the other hand key frame animations change property values abruptly at specified times, and are often used when changing certain data types, such as a string.
Using property animation to move a worm
We need to move a worm horizontally along a canvas, so we need to change the "Canvas.Left" property of the worm from 0 to 750 (could be anything) over some random interval of time. "Canvas.Left" is a Double so we need to use "DoubleAnimation". The following code moves a worm from 0 to 750 somewhere between 10 and 15 seconds.
da =new DoubleAnimation(0, 750,TimeSpan.FromSeconds(rnd.Next(10, 15) % 15));
da.AccelerationRatio = rnd.NextDouble();
da.Completed += da_Completed;
da.Name ="YellowWorm";
YellowWorm.BeginAnimation(Canvas.LeftProperty, da);
There are several important properties of propertyanimation; they are:
- Duration: Sets the length of time the animation runs, from start to finish, as a Duration object (TimeSpan.FromSeconds(rnd.Next(10, 15) % 15))
- SpeedRatio: Increases or decreases the speed of the animation. The default value is 1
- AccelerationRatio and DecelerationRatio: Makes an animation nonlinear, so it starts off slow and then speeds up (by increasing AccelerationRatio) or slows down at the end (by increasing the DecelerationRatio). Both values are set from 0 to 1
- AutoReverse: If this is set to true, the animation will play out in reverse once it is complete, reversing to the original value
- FillBehavior: Determines what happens when the animation finishes. Usually, it keeps the property fixed at the ending value
- RepeatBehavior: Allows you to repeat an animation a specific number of times.
Of these we have used only AccelerationRatio so that worms show some random behavior during the course of race. In other words a worm may start fast initially bit slow down after a while, which makes the race a little more interesting.
6. Panel to place bets
The top panel of the grid is reserved for the start and the row of buttons to place various amounts of bets. Round buttons look better than rectangles (inspired from gold coins). We can make buttons round by changing the control template of the button and using a style to apply it for all buttons. We can also use triggers to generate some animations when the mouse hovers on buttons and the user clicks on it.
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<!--Border BorderThickness="4" BorderBrush="DarkGray" CornerRadius="10"-->
<Rectangle x:Name="mainButton" Fill="{TemplateBinding Background}" Stroke="DarkGray" StrokeThickness="5" RadiusX="25" RadiusY="25">
</Rectangle>
<!--/Border-->
<ContentPresenter Margin="2" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Rectangle x:Name="buttonHoverButton" Opacity="0" RadiusX="25" RadiusY="25">
<Rectangle.Fill>
<RadialGradientBrush>
<GradientStop Color="Transparent" Offset="1"/>
<GradientStop Color="Indigo" Offset="0.9"/>
<GradientStop Color="Blue" Offset="0.8"/>
<GradientStop Color="LawnGreen" Offset="0.7"/>
<GradientStop Color="Yellow" Offset="0.6"/>
<GradientStop Color="Orange" Offset="0.5"/>
<GradientStop Color="Violet" Offset="0.4"/>
<GradientStop Color="Transparent" Offset="0"/>
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsMouseOver" Value="True">
<Setter Property="BitmapEffect">
<Setter.Value>
<OuterGlowBitmapEffect GlowColor="Violet" GlowSize="10"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="Button.IsPressed" Value="True" >
<Setter TargetName="mainButton" Property="Fill" Value="Gold"/>
</Trigger>
<EventTrigger RoutedEvent="Button.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="buttonHoverButton" Storyboard.TargetProperty="Opacity" To="1"
Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="buttonHoverButton"
Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Logic to select worms and decide winners
Selecting Worm
We need to handle various mouse-related events of the worm for selecting a worm; they are: