Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Building a WPF Sudoku Game: Part 3 - Adding Polish and Customizing Controls

  Building Sudoku using Windows Presentation Foundation and XAML, Microsoft's new declarative programming language. This is the 3rd article from a series of 5 articles and focusses on adding polish and customizing controls.

Difficulty: Easy
Time Required: 1-3 hours
Cost: Free
Software: Visual C# 2005 Express Edition .NET Framework 3.0 Runtime Components Windows Vista RTM SDK Visual Studio Extensions for the .NET Framework 3.0 November 2006 CTP
Hardware:
Download: Download (note: Tasos Valsamidis has an updated version that supports Expression Blend here)
 
Note: This article has been updated to work and compile with the RTM version of the Windows SDK.
 


Welcome to the third part of my Windows Presentation Foundation tutorial! In this tutorial we'll be adding some cool UI elements to spice up the application we built last time. First, let's redo the background a little; let's add a cool pulsating gradient, like the Xbox 360:

(This picture is from a random internet search, I need to find an internal one I can use in the real article)

Ok. So how we do this? If you've scoped out the documentation already you'll know there is a RadialGradientBrush we can use, but how can we layer multiple brushes, and most importantly how can we animate them? To make this work I've replaced the GradientPanel style with a new one. The new style uses a DrawingBrush to compose a collection of geometry objects into a single image. To create the desired effect I layered a bunch of rectangles painted with radial gradients like this:

<DrawingBrush Stretch ="UniformToFill">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing>
<GeometryDrawing.Brush>
<SolidColorBrush Color ="White"/>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<RadialGradientBrush.RelativeTransform>
<TranslateTransform X ="-0.2" Y ="0"/>
</RadialGradientBrush.RelativeTransform>
<GradientStop Color="#850039d6" Offset ="0"/>
<GradientStop Color="#654577Ff" Offset ="0"/>
<GradientStop Color="#850039d6" Offset ="0.33"/>
<GradientStop Color="#654577FF" Offset ="0.66"/>
<GradientStop Color="#850039D6" Offset ="1"/>
</RadialGradientBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>

--Code Shortened…--

 

    </DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>

 

The full version of the code included in the source files produces a background like this (I've removed the controls so you can see it better)


Next, we can use a feature of the framework called triggers to create an animation to make the background pulse. Essentially, a trigger definition tells WPF to connect a cause and effect, or in other words, “when A happens do B”. Triggers can fire in response to events or a certain value of one or more properties. A trigger can then start, stop or pause animations, or set the value of a property. This is great because it means you only really have to write code if you want to do anything exotic, in this case we just want to start an animation on the “Loaded” event of our panel.

 

<Style.Triggers>
<EventTrigger RoutedEvent ="Control.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard >
<ColorAnimation RepeatBehavior="Forever" Duration ="0:0:4"
Storyboard.TargetProperty ="Background.Drawing.Children[1].Brush.GradientStops[0].Color"
From ="#654577Ff"/>
<DoubleAnimation RepeatBehavior="Forever" Duration="0:0:4"
Storyboard.TargetProperty ="Background.Drawing.Children[1].Brush.GradientStops[0].Offset"
From ="0" To ="0.33"/>

 

--Code Shortened…--

 

        </Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>

 

From the code it should be pretty evident what's going on. The properties of the GradientStop objects are being animated in a loop. The Duration property is specified in h:m:s so the animation runs for 4 seconds and the From and To properties specify the beginning and ending values of the animated property. If you omit one of these, the current value at the beginning of the animation is used. The only other thing here probably needs explanation is the TargetProperty syntax. Since the storyboard is part of a trigger that is specified in a style, it has to point to the object the style is applied to. Since we need to affect a property a few levels down the hierarchy we can use a relative property path. The concept here is identical to how filenames work; if we are in the GradientStop object we can just reference the Color property directly. If we move up to the LinearGradientBrush we have to specify that the property we want is a member of Brush property of our current scope. (If you run the application and find that it runs unbearably slow with the animation code added, this could be due to WPF falling back to software-only mode. Upgrading your video drivers might help, but if all else fails, just comment out the triggers block)

Next, let's make the expander controls we originally styled in article 1 look better and remove the need for the extra border kludge. To do this we need to modify the control's template, the XAML that defines the visual aspects of the control. Because this XAML is completely separate from the code used to create the control, we can redefine it easily. In fact, all the standard controls are really implemented through default control templates. The C# code behind them merely implements the “concept” of the control, for example, you can create any control that can be pressed, by creating a new template for the Button control. What's great about this is that we don't have to waste time duplicating the control's logic, events, or properties, and can change only what we want, the look. The easiest way to start working with control templates is to start with the default template and modify it. The easiest way to do this, since the default template is compiled into the control, is to dump the XAML to disk with a short code fragment (which you can then remove):

 

System.Xml.XmlTextWriter xtr = new System.Xml.XmlTextWriter
(@"c:\temp.xaml",System.Text.Encoding.UTF8);
System.Windows.Markup.XamlWriter.Save(MyControl.Template, xtr);
xtr.Close();

(If you have Expression Interactive Designer, it also has a feature to do this.) The template for the expander is pretty lengthy since it deals with other cases we don't care about like left-to-right expansion, so I won't show the code here, since your eyes would probably just glaze over and you know where to get it. Essentially, what's important about this code is it tells us the basic structure of the control, there is a DockPanel with a ToggleButton that's made to not look like a button and that contains the arrow and the header. The content of the expander fills the DockPanel. In the toggle button I've replaced the default circle/arrow thing with glassy looking button:

 

<Ellipse x:Name ="BackGrad" Height="19" Width="19" HorizontalAlignment="Center" 
VerticalAlignment ="Center" StrokeThickness="1" Stroke ="DarkBlue">
<Ellipse.Fill>
<LinearGradientBrush StartPoint ="0,0" EndPoint ="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color ="LightSkyBlue" Offset ="0"/>
<GradientStop Color ="Blue" Offset ="0.5"/>
<GradientStop Color ="LightSkyBlue" Offset ="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Height="19" Width="19" HorizontalAlignment="Center"
VerticalAlignment ="Center" Stroke ="Transparent" x:Name ="FadeMask"
StrokeThickness="1" Opacity ="0">
<Ellipse.Fill>
<SolidColorBrush Color ="AliceBlue"/>
</Ellipse.Fill>
</Ellipse>
<Path Stroke="White" StrokeThickness="2" HorizontalAlignment="Center"
VerticalAlignment="Center" x:Name="Arrow" SnapsToDevicePixels="False"
Data="M1,1.5 L4.5,5 8,1.5"/>
<Ellipse Height="19" Width="19" HorizontalAlignment="Center" VerticalAlignment ="Center"
x:Name ="Highlight" StrokeThickness ="2" Stroke ="Transparent">
<Ellipse.Fill>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<LinearGradientBrush Opacity ="0.6" StartPoint ="0,0" EndPoint ="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color ="White" Offset ="0"/>
<GradientStop Color ="#AAFFFFFF" Offset ="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<EllipseGeometry Center ="0.5,0.5" RadiusX ="0.25" RadiusY ="0.35"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
<DrawingBrush.RelativeTransform>
<ScaleTransform CenterX ="0.5" CenterY ="0" ScaleX ="1" ScaleY ="0.5"/>
</DrawingBrush.RelativeTransform>
</DrawingBrush>
</Ellipse.Fill>
</Ellipse>

This code creates a sandwich of 3 ellipses and the arrow graphic to create the button face:


Next, I defined some triggers

 

<ControlTemplate.Triggers>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="LayoutTransform" TargetName="arrow">
<Setter.Value>
<ScaleTransform ScaleY ="-1"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property ="ToggleButton.IsPressed" Value ="True">
<Setter TargetName ="BackGrad" Property ="RenderTransform">
<Setter.Value>
<TranslateTransform X ="0" Y ="1"/>
</Setter.Value>
</Setter>
</Trigger>

 

These two triggers, respond to property changes. When the toggle button isn't checked (the control isn't expanded), the arrow if flipped vertically to indicate this. Also, when the button is pressed, the ellipse shifts down a pixel to look “pressed” and provide feedback to the user that their click was received.

 

  <EventTrigger RoutedEvent ="UIElement.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName ="FadeMask"
Storyboard.TargetProperty ="Opacity" To ="0.4"
Duration ="0:0:0.25"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent ="UIElement.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName ="FadeMask"
Storyboard.TargetProperty ="Opacity" To ="0"
Duration ="0:0:0.25"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>

 

These triggers animate a mouse-over effect on the glassy button. When the mouse enters and exits the header they start animations that fade the FadeMask ellipse in and out. The animations have no From properties so that if the user moves the mouse away before the animation completes the fade out animation will smoothly start instead of jumping to full brightness then fading out. Next, I wrapped the ContentPresenter, the control that displays the expander's contents, in an extra border to implement the effect we want:

 

<Border x:Name="ExpandSite" Margin ="5,0,5,5" Background ="#77FFFFFF" 
BorderBrush="{TemplateBinding Border.BorderBrush}"
BorderThickness="{TemplateBinding Border.BorderThickness}">
<Border.LayoutTransform>
<ScaleTransform ScaleY ="0"/>
</Border.LayoutTransform>
<ContentPresenter Margin="{TemplateBinding Control.Padding}"
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
Focusable="False" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
Content="{TemplateBinding ContentControl.Content}" DockPanel.Dock="Bottom">
</ContentPresenter>
</Border>

The location is important, before we were modifying the ToggleButton that makes up the header, so we edited its template, which is specified in the Expander's template. You'll also notice that I removed unhidden the control and instead included a scale by 0, the effective does the same thing. This is because I'm about to animate the control's expansion and contraction. To do this I've placed this code in the Expander's control template.

 

<ControlTemplate.Triggers>
<Trigger Property="Expander.IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName ="ExpandSite"
Storyboard.TargetProperty ="LayoutTransform.ScaleY"
To ="1" Duration ="0:0:0.25"/>
<DoubleAnimation Storyboard.TargetName ="ExpandSite"
Storyboard.TargetProperty="Opacity"
To ="1" Duration ="0:0:0.25"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName ="ExpandSite"
Storyboard.TargetProperty ="LayoutTransform.ScaleY"
To ="0" Duration ="0:0:0.25"/>
<DoubleAnimation Storyboard.TargetName ="ExpandSite"
Storyboard.TargetProperty="Opacity"
To ="0" Duration ="0:0:0.25"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>

This code shrinks and fades out the contents of the expander as it contracts and does the reverse when it expands. For the same reason as above the animations have no From property. Finally, let's modify the style to add a drop shadow (since drop shadows are cool). To do this you need to set the BitmapEffect property to a DropShadowBitmapEffect like so:

 

<Setter Property ="BitmapEffect">
<Setter.Value>
<DropShadowBitmapEffect Opacity ="0.5"/>
</Setter.Value>
</Setter>


It's that easy...ok well, its easy compared to doing it without WPF. The only caveat I should mention is that, in the current version of WPF, if you apply a bitmap effect to a control it forces that control to render in software mode, that should be fine for our simple controls, but for more complex controls it's a significant performance hit. (There are some tricks to lessen the impact of this, which I'll cover later though.) With the new expanders the program looks like this:

You can also define more complex animations in XAML. To add a cheesy bounce effect to the Sudoku title text I modified the code like this:

 

<TextBlock Foreground ="White" Margin ="5" DockPanel.Dock ="Top"
FontSize ="36" Text ="Sudoku">
<TextBlock.RenderTransform>
<TranslateTransform X="800"/>
</TextBlock.RenderTransform>
<TextBlock.BitmapEffect>
<DropShadowBitmapEffect/>
</TextBlock.BitmapEffect>
<TextBlock.Triggers>
<EventTrigger RoutedEvent ="Control.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames BeginTime ="0:0:0.25"
Storyboard.TargetProperty="RenderTransform.X"
AccelerationRatio="0.25">
<LinearDoubleKeyFrame Value="800" KeyTime="0:0:0"/>
<LinearDoubleKeyFrame Value="-30" KeyTime="0:0:0.3"/>
<LinearDoubleKeyFrame Value="20" KeyTime="0:0:0.5"/>
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:0.6"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>

As you can see, adding animations this way is pretty straightforward, and is almost identical to non-key-framed animations. The only new stuff is the use of the BeginTime property that allows you to start the animation after the event occurs, in this case it is used to allow the expanders to drop down before the title slides in. Also, the AccelerationRatio specifies the fraction of the total time the object spends accelerating to its maximum velocity. A value of 1.0 means a smooth acceleration and a value of 0 means no acceleration at all.

Ok, so after taking a detour defining some styles, it's time to get more of the basic game mechanics working. First let's modify the SudokuBoard class to allow the board to be altered externally. In reality, the GameBoard field contains the same thing as the data context of the main list, so instead of synchronizing them, we can eliminate this value altogether.

 

public partial class SudokuBoard : UserControl
{
public Board GameBoard
{
get
{
return MainList.DataContext as Board;
}

set
{
MainList.DataContext = value;
}
}

public SudokuBoard()
{
InitializeComponent();
this.GameBoard = new Board(9);
}
}

Next, let's add a new method in the Board class to generate a board. The algorithm I use randomly places givens, checks if they are valid and if not removes them and tries again until it has placed enough. This algorithm sucks, but it is fast, we'll add a better one late through plug-ins. Next, if we modify the size selection drop-down to contain integers instead of ComboBoxItems then it will be easy to make the “New Game” button functional. We can link into the System namespace in the CLR and use the integer type directly from there, in fact we can link into any namespace and use any type!

 

<ComboBox x:Name ="BoardSize" xmlns:s="clr-namespace:System"
ItemTemplate ="{StaticResource BoardSizeTemplate}" IsEditable ="False">
<ComboBox.SelectedItem>
<s:Int32>9</s:Int32>
</ComboBox.SelectedItem>
<s:Int32>9</s:Int32>
<s:Int32>16</s:Int32>
<s:Int32>25</s:Int32>
<s:Int32>36</s:Int32>
</ComboBox>

 

Using a data template like this:

 

<DataTemplate x:Key ="BoardSizeTemplate">
<StackPanel Orientation ="Horizontal" FlowDirection ="LeftToRight">
<TextBlock Text ="{Binding}"/>
<TextBlock Text ="x"/>
<TextBlock Text ="{Binding}"/>
</StackPanel>
</DataTemplate>

The appearance is identical to the user but it allows the behind the UI to easily work without having to hardcode the items in the list into it. The event handler for the “New Game” button is very simple; the data binding does most of the work:

 

void NewGameClicked(object sender, RoutedEventArgs e)
{
Board.GameBoard = new Board((int)BoardSize.SelectedItem);
Board.GameBoard.GenerateGame((int)BoardSize.SelectedItem *2);
}

Finally, to make the Sudoku game playable, we need to add some triggers to the cell data template:

 

<DataTemplate.Triggers>
<DataTrigger Binding ="{Binding IsValid}" Value ="False">
<Setter TargetName ="Border" Property ="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding ="{Binding ReadOnly}" Value ="True">
<Setter TargetName ="Border" Property ="Background" Value="Blue"/>
<Setter TargetName ="Border" Property="ContextMenu" Value ="{x:Null}"/>
</DataTrigger>
</DataTemplate.Triggers>

Data triggers perform an action based on the property of a data source, in this case the triggers change the background of the cell depending on whether it's a given cell or not and to mark invalid cell values. The context menu of a read-only cell is also removed so its value cannot be altered.

Not that cool looking, but it does the job. In the sample application, I've replaced the solid color fills with better looking effects in the sample app but layering up gradients is just more of the same so I'll move on. I've also added a drop shadow to the board, but I did this slightly differently then before. Because adding a drop shadow to a control causes it to render in software-mode, sometimes it's worth it to cheat, especially for large, square controls. I layered an empty panel behind the board and gave it a drop shadow. This has the same effect but avoids the need to process the board graphic itself. I also used this trick to in the template for the expander control; I've included the modified version in the download.

Another cool effect you can do with very little code is control reflections. Let's add a reflection to the title text. First, since we want the reflection to follow the title's animation, we need to refactor the animation from the TextBlock to a Grid surrounding the text. Then we need to add a rectangle to the grid so that it sits over the text.

 

<Rectangle VerticalAlignment ="Top" HorizontalAlignment ="Left" 
x:Name ="TitleReflect"
Width ="{Binding ElementName=TitleText,Path=ActualWidth}"
Height ="{Binding ElementName=TitleText,Path=ActualHeight}">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform CenterY="{Binding ElementName=TitleText,Path=ActualHeight}"
ScaleY ="-1"/>
<TranslateTransform Y ="-10"/>
</TransformGroup>
</Rectangle.RenderTransform>
<Rectangle.OpacityMask>
<LinearGradientBrush StartPoint ="0,0" EndPoint ="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color ="#00112233" Offset ="0.3"/>
<GradientStop Color ="#a0112233" Offset ="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.OpacityMask>
</Rectangle>

The rectangle is flipped and transformed vertically to the correct position using a render transform. Using a render transform as opposed to a layout transform is important because render transforms are evaluated after the controls have been arranged. This way the reflection rectangle does actually take up any extra space below the text in the window layout. Also new here is the use of an opacity mask, which is a brush that is used to mask out areas in the control. For each pixel drawn, the opacity value is taken from the opacity mask brush, whose other components are ignored. Next, we need to create an event hander for the window's Loaded event in which we hook up the rectangle to the text:

 

void WindowLoaded(object sender, RoutedEventArgs e)
{
TitleReflect.Fill = new VisualBrush(TitleText);
}

We do this by filling the rectangle with a VisualBrush, which paints an area with a Visual. Since most controls inherit from Visual, this allows you to essentially paint an area with the dynamic image of a control. This can do more than just reflection, you could build a magnifier control or even paint other controls on a 3D object. This unification of the graphics system is great example of what's cool about WPF!

Well, you can play Sudoku now, but we're still not done! Stay tuned for the next tutorial where I'll be going over:

· Creating more complex custom controls, inheriting from existing controls, and defining custom properties that can be animated or data bound

· Updating the UI to be more functional and to have a real timer

· Building a Sudoku solver as a plug-in

· Running solver plug-ins with least privilege

See you next time!

Tags:

Follow the Discussion

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.