XTab's Blog

Ged Mead's Blog at vbCity

This blog hosted by:
http://blogs.vbcity.com      
  Home :: Syndication  :: Login

SepOctober 2007Nov
SMTWTFS
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

Archives

Topics

Ramblings

VB.NET

Monday, October 01, 2007 #

Introduction

  Control Templates are a particularly powerful feature of WPF. They allow you to change almost any aspect of the look of a control, but - and here's the key benefit - you still retain every bit of the functionality pre-built into that control. So you can create, for example, a button that doesn't look anything like the rectangular one we are all used to, but when the user clicks on your version of the button you will have access to the button's events, such as the click event and can code against it, just as you would normally expect to.

Button Control Template Example

  I recently needed a button that looked like the image below for a project I was working on:

 

 In fact, I needed a set of them which I wanted to lay out in a general diamond shape:

 

Step 1: Create the Shape

  Although it would be possible to hand-build the polygon as a WPF shape in Visual Studio, I took the easy route, opened up Expression Blend and drew the outline of the button shape that I wanted:

 Switching from the Design view to the XAML view in Expression Blend reveals the Path I needed in order to recreate this shape. Having the copy of Blend available is a huge benefit in situations like this.

 

Step 2: Define the Gradient Colors

 We will see where the Path fits into the ControlTemplate in a moment, but first, in order to give the diamonds a more individual look and feel, I created three separate Gradient Brushes which would be applied in certain situations. These are:

    • Default
    • Mouse Over Diamond
    • Mouse Click on Diamond

 To keep the code neatly compartmentalized, I created a XAML ResourceDictionary to hold all the UI code for the diamond shaped button, including these three Brushes. (If you need guidance on how to use ResourceDictionaries, check out my vbCity Blog item here.)

 I named this ResourceDictionary "ResDiamondTile".

 Creating gradients (or more accurately, Gradient Brushes) is again a task that is made a hundred times easier in Expression Blend. You can tweak/test to your heart's content until you have just the effect you want. Of course, you can code the settings for the StartPoint, EndPoint and each GradientStop by hand in Visual Studio, but it's a relatively slow business.

 (If you want a brief introduction to some of the tasks involved in creating Gradient Brushes in code - albeit in this case a RadialGradientBrush - you can check out my article page here.)

 Having created three separate LinearGradientBrushes in Expression Blend, I transferred the graphical info into the ResourceDictionary ResDiamondTile. Here's the code:

Code Copy
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">


<
LinearGradientBrush
x:Key="DefaultTileGradient" EndPoint ="1,0.5" StartPoint="0,0.5">
<
GradientStop Color="#FF53EE27" Offset="0"/>
<
GradientStop Color="#FFFFFFFF" Offset="0.995"/>
<
GradientStop Color="#FF36F48F" Offset="0.49"/>
<
GradientStop Color="#FF4CF41C" Offset="0.788"/>
LinearGradientBrush>
<LinearGradientBrush
x:Key="MouseOverGradient"
EndPoint
="1,0.5" StartPoint="0,0.5">
<
GradientStop Color="#FF53EE27" Offset="0"/>
<
GradientStop Color="#FFFFFFFF" Offset="0.865"/>
<
GradientStop Color="#FF36F48F" Offset="0.394"/>
<
GradientStop Color="#FF4CF41C" Offset="0.663"/>
<
GradientStop Color="#FF50DA29" Offset="0.962"/>
LinearGradientBrush>
<LinearGradientBrush
x
:Key="MouseClickedGradient"
EndPoint
="1,0.5" StartPoint="0,0.8">
<GradientStop Color="#FF53EE27" Offset="0"/>
<
GradientStop Color="#FFFFFFFF" Offset="0.606"/>
<
GradientStop Color="#FF5EF5A5" Offset="0.255"/>
<
GradientStop Color="#F762F56C" Offset="0.356"/>
<
GradientStop Color="#FF50DA29" Offset="0.986"/>
LinearGradientBrush>
ResourceDictionary>

  Notice that each LinearGradientBrush is given a name (Key) that we will use to refer to each particular gradient configuration later.

 We'll get to see how each of these gradients looks in a moment, but next we need to make a start on actually creating the new template.

 

Step 3: Define The ControlTemplate - The Look
  The ControlTemplate will also go in the ResourceDictionary ResDiamondTile, the code being placed after the code for the brushes. Because the file is read top to bottom, if the brushes don't exist by the time the ControlTemplate needs them, you will get an error.

 The first task is to name this ControlTemplate and set its TargetType. "Name" in ControlTemplate is characterized by its "Key" property. The key name I will use is "DiamondTile". The TargetType is of course the Button type. Line 1 of the Template therefore looks like this:

Code Copy

<
ControlTemplate x:Key="DiamondTile" TargetType="{x:Type Button}">

 We are going to use the Path that I created in Expression Blend as the means of drawing the shape we require. We will use a Grid control to house the path. Inside the Grid we next code the properties that create the path, drawing its shape, setting its size and filling it with color:

Code Copy

<
ControlTemplate x:Key="DiamondTile" TargetType="{x:Type Button}">
<Grid>
<Path x:Name="DiamondShape"
     Data="F1 M 256,32L 352,32L 416,192L 352,352L 256,352L 192,192L 256,32 Z "
           Opacity="0.9"  Stretch="Fill"
           Stroke="#FF2A8117" Width="32" Height="42"
     Fill="{StaticResource DefaultTileGradient}" >
Path>
Grid>

 Some points to note with the above snippet:- We assigned a Name to the Path so that we can refer to it later.

 The Data that I generated in Blend is simply pasted in so that the desired path will be drawn when instances of this control are created.

 The Opacity setting is optional; it was just a tweak to soften the color block. The Width and Height are hard coded to force the shape to retain its relative proportions. The Stroke assigns the Olive Green color that is used as the outline of the shape.

 The Fill uses the DefaultTileGradient that we created earlier. So the whole of the shape that is created by the path is filled with the default gradient we defined as one of our Resources.

 In case you were wondering, it isn't strictly necessary to use a resource to assign a value for the Fill color(s). It would be quite acceptable to have entered the code that creates the GradientBrush directly here inside the ControlTemplate instead of creating a resource and pointing to it. However, the compartmentalizing often helps to make the code more readable and reusable. And of course if you were using a basic color you could simply use code such as :

Code Copy
Fill="Navy"

 We need to deal with the button's Content next. (Content can be just about anything in WPF, but the nearest equivalent in Windows Forms is the Button's Text property). Because we know in advance that this button (as most buttons do) will have some content placed on its surface, we need a way of letting that content be passed back to the template. Of course we could assign a value for the content right here in the template, but if we do that then the content is hard coded into the template and can't be changed in client code. This is just the kind of WinForms controls scenario that WPF was designed to avoid - being locked in to the preset confines of what a control will look like.

 In a ControlTemplate you need to approach the value for the control's content by using a ContentControl. This is placed inside the ControlTemplate. For demonstration purposes, if you did have a situation where you wanted to set the content in tablets of stone then the code inside the template would be as follows:

Code Copy
<ContentControl Content="X" />

The result would be (as you would hope!):-

 

 So what? you may be thinking. I'll just change the Content when I create a new Button instance with:-

Code Copy
<Button Name="buttonB" Template ="{StaticResource DiamondTile}"
Content
="B">
Button>

or the usual abbreviated version:

<
Button Name="buttonB" Template ="{StaticResource DiamondTile}">
B

Button>

 and the new content will overwrite the template's content. Not so. In this kind of battle the template always wins. So what we have to do is tell the template that we want it to accept whatever content is set by the client code. The syntax for this is as follows:

Code Copy
<ContentControl  Content="{TemplateBinding Content}" />

 which you can read as "go to the control that this template is currently bound to, get the content from there and use it as your content". Now, whatever you assign as the value for the content in any of your DiamondTile instances will be faithfully used. For my project I happen to only want to use single letters of the alphabet, but - this being WPF - you can cram as much complex content as you wish into the button:-

Code Copy
<Button Name="buttonB" Template ="{StaticResource DiamondTile}"
<Button.Content>
<StackPanel>
<Image Source="C:\Caravan.jpg" Width="18"/>
<
TextBlock FontSize="6">CaravanTextBlock>
StackPanel>
Button.Content>
Button>

 

 If at this point you're about to say that you can easily have an image and some text in a WinForms button then you are of course correct. However, you simply don't have the versatility of layout in WinForms controls that comes with WPF. Another plus point is that it's just as simple to add more images, or text above and below the image or in fact any other element you think might serve your UI purposes.

 In this particular case, the DiamondTile will only ever contain one character so we can use a WPF scaling trick to ensure that the character is sized proportionately to the size of the overall button. To do this we put it in a ViewBox (which I will cover in more detail in a future blog) and assign a value to the Margin property. This ensures that the text will grow or shrink neatly if the button is resized once it is in use in a Window.

 So the code so far is:

Code Copy
<ControlTemplate x:Key="DiamondTile" TargetType="{x:Type Button}">
<
Grid>
<Path x:Name="DiamondShape"
     Data="F1 M 256,32L 352,32L 416,192L 352,352L 256,352L 192,192L 256,32 Z "
     Opacity="0.9"  Stretch="Fill"
     Stroke="#FF2A8117" Width="32" Height="42"
     Fill="{StaticResource DefaultTileGradient}">
    Path>
<Viewbox>
<ContentControl Margin="8" Content="{TemplateBinding Content}" />
Viewbox>
Grid>


ControlTemplate>

 At this stage, the design of the button as seen by its users is complete. You can create an instance of this button in the usual XAML way and assign a value for the content. Something like:

Code Copy
<Button Name="buttonB" Template ="{StaticResource DiamondTile}">
  B

Button>

 In fact, I will wrap the button inside some other controls when we get to the final stage just to improve the look of it, but the key point to take away here is that the design is complete. The button looks as we wanted it to. By assigning the name of our ControlTemplate "DiamondTile" to this button, it will be given all the characteristics we have laid down in the template; it will look like a six sided diamond but it will perform like a button.

 

Step 4: Define The ControlTemplate - User Interaction

  Quote: "it will perform like a button"
A button needs to react to a button click, at the very minimum. It's a useless, if colorful, drawing in the Window if it isn't able to react to user actions. Before we go on with some more XAML I just want to emphasize that at this stage this button (i.e. ButtonB in the code snippet above) is now open to the kind of button events that you are familiar with from Windows Forms (albeit some of them are renamed). You can go to the code behind file - that is, the Visual Basic or C# one in the Solution Explorer and code against, for example, the click event.

 The following Visual Basic code in the window's xaml.vb file will act just as you would expect a Windows Forms event to act:

Code Copy
Partial Public Class Window3

   Private Sub buttonB_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles buttonB.Click
        Me.Title = "Where's my four pence?"
End
Sub

 That is, when the button is clicked (the Click event fires) the text of the title of the Window will change. (You have to be British and over a certain age to get the joke about what happens when you press Button B.)

 

 In exactly the same way you can code in VB against the MouseOver button event:

Code Copy
Private Sub buttonB_MouseEnter(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles buttonB.MouseEnter
    Me.Title = "Mouse Over Button B!"
End
Sub

 I've deviated slightly away from the topic of ControlTemplates to cover that, but I think it's important to remember that you don't have to twist yourself into loops just to write code in XAML if you have an easy way of achieving your aim in VB. That said, as we move on to the next stage, this is a task that is probably easier in XAML than VB.

Our next task is to change the gradients used when the cursor is over the button and when the button is pressed. Note in advance that although we have reacted to the MouseEnter and Click events in the demos above, we are still able to force changes to the look of the control at the same time and because of the same user action in the XAML code.

 To react to user actions in XAML we use Property Triggers. It takes a bit of a mind shift to accept that as well as there being a MouseEnter event, we also have access to the result of that event - that is, the button's "IsMouseOver" property. This changes from False to True when the mouse is moved over the button. We can harness this IsMouseOver dependency property to change the gradient. The XAML code for this is relatively simple and - one of the benefits of XAML in this kind of scenario - very human readable.

 The next block of the ControlTemplate looks like this:

Code Copy

<
ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="DiamondShape"
Property
="Fill" Value="{StaticResource MouseOverGradient}" />

<
Setter TargetName="DiamondShape" Property="RenderTransform">
<Setter.Value>
<
ScaleTransform ScaleX="1.02" ScaleY="1.02" />
Setter.Value>
Setter>
Trigger>
ControlTemplate.Triggers>

 Let's see what is happening here. The Triggers are contained in a ControlTemplate.Triggers collection.
The Trigger has to target a property; in this case the IsMouseOver Boolean property. We only want the gradient to change when this value remains true (i.e. only as long as the mouse is over the button). So we set the Value that will fire this trigger to True. One of the advantages of the property trigger in WPF is that it works just like that; as soon as the value changes back to False then the "MouseOverGradient" we use here is no longer applied.

The next line creates a Setter element which as you would expect is employed to set the details of what will happen when this Trigger is fired. Recall that what we want to do is to change the gradient applied to the button, but bear in mind that the button's shape is set by that Path we used earlier. That Path, which we named "DiamondShape", has a default gradient applied to its Fill Property. Now we will change the value of that Fill property in this trigger. This line:

Code Copy

<
Setter TargetName="DiamondShape"


 defines the target that we want to aim our change at - DiamondShape Path. This line:

Code Copy

Property
="Fill" Value="{StaticResource MouseOverGradient}" />




 defines the Property to be targeted and sets the Value to be assigned. In this case, of course, it is the Fill property and we assign the second of our previously prepared GradientBrushes, the one we named "MouseOverGradient".

 With this code in place, every time the mouse is moved over one of our DiamondTile buttons the gradient will change:

 It's a relatively subtle change, but quite enough to be noticeable.

 The next block of code in that Trigger targets the same Path, of course, but this time it increases the overall size of the button by 2%. It does this by transforming the values of the path in both the X and Y axes. Again it is not a large change but enough to draw attention that something is happening. The size change effect is unnoticeable in the following screenshot, but of course it's the movement that catches the eye when the application is run, not just the difference in size.

 

 Note that both the Triggers are effective when the mouse is over the button. You could add further Setters if you needed to.

 The final part of the ControlTemplate's code deals with another Dependency Property, the IsPressed property. All the logic as described for the IsMouseOver property applies equally here, e.g. the effect remains in force as long as the IsPressed property remains True, but stops as soon as it becomes False.

Code Copy
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="DiamondShape"
Property="Fill" Value="{StaticResource MouseClickedGradient}" />
<Setter TargetName="DiamondShape" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.98" ScaleY="0.98" />
Setter.Value>
Setter>
Trigger>




 As you would expect, the Setters target the DiamondShape path again. There are two Setters (although again there could be more - or less). The first one applies the third gradient to the shape. The second one reduces the overall size by 2% and this reinforces the feeling that the button is being pressed.

 

Summary

 So putting this all together, the ControlTemplate code looks like this:

Code Copy

<
ControlTemplate x:Key="DiamondTile" TargetType="{x:Type Button}">
<Grid>
<Path x:Name="DiamondShape"
       Data="F1 M 256,32L 352,32L 416,192L 352,352L 256,352L 192,192L 256,32 Z "
            Opacity="0.9"  Stretch="Fill"
            Stroke="#FF2A8117" Width="32" Height="42"
      Fill="{StaticResource DefaultTileGradient}">
    Path>
<Viewbox>
<ContentControl Margin="8" Content="{TemplateBinding Content}" />
Viewbox>
Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="DiamondShape"
Property
="Fill" Value="{StaticResource MouseOverGradient}" />

<
Setter TargetName="DiamondShape" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="1.02" ScaleY="1.02" />
Setter.Value>
Setter>
Trigger>

<
Trigger Property="IsPressed" Value="True">
<Setter TargetName="DiamondShape"
Property
="Fill" Value="{StaticResource MouseClickedGradient}" />
<Setter TargetName="DiamondShape" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.98" ScaleY="0.98" />
Setter.Value>
Setter>
Trigger>
ControlTemplate.Triggers>
ControlTemplate>




 The final step is to create a few buttons in a Window. For the effect I needed I placed them on a Canvas and kept them scaled with a ViewBox. The code I used was:

Code Copy
<Window x:Class="Window3"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window3" Height="400" Width="400">
<
Grid>
<Viewbox>
<Canvas Width="150" Height="105" Background="Black">
<Button Name="buttonA" Template ="{StaticResource DiamondTile}"
Canvas.Left
="55" Canvas.Top="8" >
A
Button>

<
Button Name="buttonB" Template ="{StaticResource DiamondTile}"
Canvas.Left
="31" Canvas.Top="29" >
B
Button>
<Button Name="buttonC" Template ="{StaticResource DiamondTile}"
Canvas.Left="55" Canvas.Top="50" >
C
Button>
<Button Name="buttonD" Template ="{StaticResource DiamondTile}"
Canvas.Left="79" Canvas.Top="29" >
D
Button>
Canvas>
Viewbox>
Grid>
Window>

 And the final result was:

 

 The button template can be reused as-is in this or other projects. I can devise other gradients if the next window has, for instance, a blue theme to it. I could create another Path for a completely different shape and replace the Path Data. With a little work, I could use the shape and the gradients and apply them to other controls. In short, once you have the blueprint laid down for your template, it is a highly reusable piece of code that may help you create better user interfaces.

posted @ 11:02 AM