2008-04-25

WPF custom button templates


WPF Button

I’ve had a bit of a play with WPF for the first time today. I decided to create my own button template. The idea of a template is that you can redefine the visual elements that make up the control.

Create a new WPF application

Now add a button within the grid like so

 <Grid>
  <Button Content="Click me" Width="150" Height="50"/>
 </Grid>

So that we can see what we are designing add a gradient background to the window. Within the <Window> node add

 <Window.Background>
  <LinearGradientBrush>
   <GradientStop Color="Black" Offset="0"/>
   <GradientStop Color="White" Offset="1"/>
  </LinearGradientBrush>
 </Window.Background>


Now to start designing the button template. Within the <Window> node add
 <Window.Resources>
 </Window.Resources>

this is where we will add the template, within that new node add the following

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

and then change the button to use this template

 <Button Content="Click me" Width="150" Height="50" Template="{DynamicResource PetesButton}"/>


This creates a control template that targets the "Button" control. As a result the button will disappear, this is because our button template is currently empty. To add a rounded rectangle as the outline for the button by adding a <Border> element.

 <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="Black" Background="Black">
 </Border>

Next a <Grid> will be added within that new <Border> element, this is because by default a grid will stack controls on top of each other in a Z order rather than laying them out vertically or horizontally. Within the <Grid> add another border which will act as the client area of the control

 <Grid>
  <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Background="White">
  </Border>
 </Grid>

Next add a nice gradient to this border like so
 
 <Border.BorderBrush>
  <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
   <GradientStop Color="White" Offset="0"/>
   <GradientStop Color="#222222" Offset="1"/>
  </LinearGradientBrush>
 </Border.BorderBrush>

This sets the BorderBrush property for the Border to a LinearGradient which starts at 0.5,0 (X=50%, Y=0%) and ends at 0.5,1 (X=50%, Y=100%). The gradient is made up of two parts, White at offset 0 (the very beginning of the gradient) and #222222 at offset 1 (the very end of the gradient).

As this gradient looks so nice lets remove the Background="White" from the <Border> and inside it add a LinearGradient to the background

 <Grid>
  <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
   <Border.Background>
    <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
     <GradientStop Color="#aaaaff" Offset="0"/>
     <GradientStop Color="#444466" Offset="0.6"/>
     <GradientStop Color="#444444" Offset="1"/>
    </LinearGradientBrush>
   </Border.Background>
  </Border>
 </Grid>


This does exactly the same as the BorderBrush, except there is an addition colour at offset 0.6 (60% of the way along the gradient).

Now we have a pretty looking button, but where is the text? To show the text we need a <ContentPresenter>. Beneath the "BorderUp" element (and within <Grid>) add the following

 <ContentPresenter x:Name="Contents" HorizontalAlignment="Center" VerticalAlignment="Center" Width="Auto" Margin="3,3,3,3"/>

Now you will see the text appear! Run the app, the button looks nice! Click the button, now it looks rubbish! :-)

Below the "BorderUp" element and above the "Contents" element add another border that is the reverse of BorderUp, by this I mean that the colours are switched in the gradient, name it "BorderDown"

 <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
  <Border.BorderBrush>
   <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
    <GradientStop Color="White" Offset="1"/>
    <GradientStop Color="#222222" Offset="0"/>
   </LinearGradientBrush>
  </Border.BorderBrush>
  <Border.Background>
   <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
    <GradientStop Color="#aaaaff" Offset="0"/>
    <GradientStop Color="#444466" Offset="0.6"/>
    <GradientStop Color="#444444" Offset="1"/>
   </LinearGradientBrush>
  </Border.Background>
 </Border>

The button now looks like it is sunk into the form. Change the BorderDown element so that it has no opacity

 <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Opacity="0">

Now to make this sunken border visible when the mouse is pressed down. Just before the closing </ControlTemplate> node add


Within that node add a trigger that reacts to the IsPressed property changing

 <ControlTemplate.Triggers>
  <Trigger Property="IsPressed" Value="True">
   <Setter TargetName="BorderDown" Property="Opacity" Value="1"/>
   <Setter TargetName="Contents" Property="Margin" Value="4,4,2,2"/>
  </Trigger>
 </ControlTemplate.Triggers>


Now try running the app and clicking the button. Much nicer, but I think we can improve on it some more!


Above the </ControlTemplate.Triggers> closing node add

 <ControlTemplate.Resources>
 </ControlTemplate.Resources>

Within this section add a <StoryBoard> node with the name "MouseDownTimeLine"

 <Storyboard x:Name="ButtonDownTimeLine">
 </StoryBoard>

Now we are going to animate a property of type Double, to do this we need a DoubleAnimationUsingKeyFrames node

 <Storyboard x:Key="ButtonDownTimeLine">
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
   <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
  </DoubleAnimationUsingKeyFrames>
 </Storyboard>

and also the opposite, an animation setting the opacity of BorderDown back to zero

 <Storyboard x:Key="ButtonUpTimeLine">
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
   <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
  </DoubleAnimationUsingKeyFrames>
 </Storyboard>

Run the app and click the button. Notice how the same old behaviour is there, this is because we haven’t hooked up the StoryBoard animations to anything yet! Change the <ControlTemplate.Triggers> so that it reads as follows

 <ControlTemplate.Triggers>
  <Trigger Property="IsPressed" Value="True">
   <Trigger.EnterActions>
    <BeginStoryboard Storyboard="{StaticResource ButtonDownTimeLine}"/>
   </Trigger.EnterActions>
   <Trigger.ExitActions>
    <BeginStoryboard Storyboard="{StaticResource ButtonUpTimeLine}"/>
   </Trigger.ExitActions>
  </Trigger>
 </ControlTemplate.Triggers>

This will now start the relevant animation whenever the trigger occurs. However, we have lost some behaviour. Previously the contents would offset when the button was down. This was achieved by changing the margin of the "Contents" from 3,3,3,3 to 4,4,2,2 - to reintroduce this behaviour we need to add another animation within the two StoryBoards. As this property is of type Thickness we will need a <ThicknessAnimationUsingKeyFrames> node.

 <ControlTemplate.Resources>
  <Storyboard x:Key="ButtonDownTimeLine">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
    <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
   <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
    <SplineThicknessKeyFrame KeyTime="00:00:00.025" Value="4,4,2,2"/>
   </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
  <Storyboard x:Key="ButtonUpTimeLine">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
    <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
   </DoubleAnimationUsingKeyFrames>
   <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
    <SplineThicknessKeyFrame KeyTime="00:00:00.25" Value="3,3,3,3"/>
   </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
 </ControlTemplate.Resources>

Now the margin will also animate.


Here is the XAML in its entirety, just so I can copy/paste it myself at a later date :-)

    <Style TargetType="{x:Type Button}">
      <Setter Property="Foreground" Value="White"/>
    </Style>
    <ControlTemplate x:Key="GlassButton" TargetType="{x:Type Button}">
      <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="Black" Background="Black">
        <Grid>
          <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
            <Border.BorderBrush>
              <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
                <GradientStop Color="White" Offset="0"/>
                <GradientStop Color="#222222" Offset="1"/>
              </LinearGradientBrush>
            </Border.BorderBrush>
            <Border.Background>
              <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                <GradientStop Color="#aaaaff" Offset="0"/>
                <GradientStop Color="#444466" Offset="0.6"/>
                <GradientStop Color="#444444" Offset="1"/>
              </LinearGradientBrush>
            </Border.Background>
          </Border>
          <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Opacity="0">
            <Border.BorderBrush>
              <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
                <GradientStop Color="White" Offset="1"/>
                <GradientStop Color="#222222" Offset="0"/>
              </LinearGradientBrush>
            </Border.BorderBrush>
            <Border.Background>
              <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                <GradientStop Color="#aaaaff" Offset="0"/>
                <GradientStop Color="#444466" Offset="0.6"/>
                <GradientStop Color="#444444" Offset="1"/>
              </LinearGradientBrush>
            </Border.Background>
          </Border>
          <ContentPresenter x:Name="Contents" HorizontalAlignment="Center" VerticalAlignment="Center" Width="Auto" Margin="3,3,3,3"/>
        </Grid>
      </Border>
      <ControlTemplate.Resources>
        <Storyboard x:Key="MouseDownTimeLine">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
            <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
          </DoubleAnimationUsingKeyFrames>
          <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
            <SplineThicknessKeyFrame KeyTime="00:00:00.025" Value="4,4,2,2"/>
          </ThicknessAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="MouseUpTimeLine">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
            <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
          </DoubleAnimationUsingKeyFrames>
          <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
            <SplineThicknessKeyFrame KeyTime="00:00:00.25" Value="3,3,3,3"/>
          </ThicknessAnimationUsingKeyFrames>
        </Storyboard>
      </ControlTemplate.Resources>
      <ControlTemplate.Triggers>
        <Trigger Property="IsPressed" Value="True">
          <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource MouseDownTimeLine}"/>
          </Trigger.EnterActions>
          <Trigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource MouseUpTimeLine}"/>
          </Trigger.ExitActions>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>

9 comments:

Anonymous said...

Thank you so muc!!!!!!!
that was awsome

it's an amazing tchnology!!

however you don't need to use animation with key frames; you could use simple animations as well (sorry Html tags is not allowed here):

DoubleAnimation Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:.08"

Ramsey said...

Thank you!

Shimmy said...

Is there anywhere I can download this sample from??

J R M said...

This is beautiful and easily changed for me (which I'm always grateful for!) Thank you so much for sharing!

Marine said...

so cool man !!!
I was looking for so long for a good tutorial where things were well explained thanks !!

Nick said...

Awesome with the explanation!! Keep up the good work

Nick said...

Awesome thanks alot

Rakhi Gulati said...

How to use storyboard for all buttons. What I need is to define a storyboard to bounce a button. all the buttons should have bounce effect and i dont want to write code to put effect on every button.

Gangsta said...

Very Nice .,,,