Tuesday, 10 July 2007

Creating a Custom WPF Button Template in XAML

In this post I will demonstrate how to create a custom template for a WPF button using XAML. In particular we will look at being able to have complete control over all the visual states, including disabled, mouse over, mouse down and even the appearance of the focus rectangle.

Stage 1: Creating a style

The first thing to do is to create a Style which sets the properties of the Button that we want to customize. We will put this in the Page.Resources section of our XAML file.

<Style x:Key="OrangeButton" TargetType="Button">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Margin" Value="2"/>
<Setter Property="FontFamily" Value="Verdana"/>
<Setter Property="FontSize" Value="11px"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource MyFocusVisual}" />
<Setter Property="Background" >
   <Setter.Value>
       <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" >
           <GradientStop Color="#FFFFD190" Offset="0.2"/>
           <GradientStop Color="Orange" Offset="0.85"/>
           <GradientStop Color="#FFFFD190" Offset="1"/>
       </LinearGradientBrush>
   </Setter.Value>
</Setter>

This is all fairly straightforward stuff - I am just setting the font and background gradient. The only complicated bit is that I am overriding the focus rectangle drawing, as I want a smaller rectangle than the one that gets drawn by default. So I need another style in my Page.Resources section:

<Style x:Key="MyFocusVisual">
     <Setter Property="Control.Template">
       <Setter.Value>
         <ControlTemplate TargetType="{x:Type Control}">
             <Grid Margin="3 2">
               <Rectangle Name="r1" StrokeThickness="1" Stroke="Black" StrokeDashArray="2 2"/>
               <Border Name="border" Width="{TemplateBinding ActualWidth}" Height="{TemplateBinding ActualHeight}"  CornerRadius="2" BorderThickness="1" />
             </Grid>
         </ControlTemplate>
       </Setter.Value>
     </Setter>
   </Style>

Stage 2: Creating a template

We would already be finished if I just wanted the normal appearance of my button to be changed. But I want complete control - including the appearance in mouse over, mouse down and disabled situations. So a template allows us to completely control what visual elements constitute our control.

Again this goes in Page.Resources. The first part is shown here:

<Setter Property="Template">
   <Setter.Value>
       <ControlTemplate TargetType="Button">
           <Border Name="border"
               BorderThickness="1"
               Padding="4,2"
               BorderBrush="DarkGray"
               CornerRadius="3"
               Background="{TemplateBinding Background}">
               <Grid >
               <ContentPresenter HorizontalAlignment="Center"
                              VerticalAlignment="Center" Name="contentShadow"
                   Style="{StaticResource ShadowStyle}">
                   <ContentPresenter.RenderTransform>
                       <TranslateTransform X="1.0" Y="1.0" />
                   </ContentPresenter.RenderTransform>
               </ContentPresenter>
               <ContentPresenter HorizontalAlignment="Center"
                           VerticalAlignment="Center" Name="content"/>
               </Grid>
           </Border>

Here you can see I have set up a simple border which gives my button rounded corners, a single pixel gray border, and uses the fill from the control's Background property. This means that by default it will use the orange gradient specified in my style, but that users can override this for their own Background.

To draw the content (usually text), we use a ContentPresenter control. I am trying to do something clever here, which is create a bevelled effect on the text by drawing it again in gray. This works fine on text, but for some reason doesn't do anything when the Content is a Shape. I'm not sure why that is. ShadowStyle is defined as follows:

<Style x:Key="ShadowStyle">
   <Setter Property="Control.Foreground" Value="LightGray" />
</Style>

Stage 3: Applying some triggers

Now we need to add some triggers to our Button template so that we can change its appearance on various events.

First is mouse over. When IsMouseOver becomes true, we change the colour of the border and the text colour to blue. Unfortunately, setting the Foreground property does nothing if the content is a shape.

<ControlTemplate.Triggers>
  <Trigger Property="IsMouseOver" Value="True">
     <Setter TargetName="border" Property="BorderBrush" Value="#FF4788c8" />
     <Setter Property="Foreground" Value="#FF4788c8" />
  </Trigger>

Next is mouse down. When IsPressed becomes true, we modify the background gradient so it looks like the button has gone 'down'. I also move the text down one pixel.

<Trigger Property="IsPressed" Value="True">                   
   <Setter Property="Background" >
   <Setter.Value>
       <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" >
           <GradientStop Color="#FFFFD190" Offset="0.35"/>
           <GradientStop Color="Orange" Offset="0.95"/>
           <GradientStop Color="#FFFFD190" Offset="1"/>
       </LinearGradientBrush>
   </Setter.Value>
   </Setter>
   <Setter TargetName="content" Property="RenderTransform" >
   <Setter.Value>
       <TranslateTransform Y="1.0" />
   </Setter.Value>
   </Setter>
</Trigger>

Third, we draw a dark gray border when a button is focussed or if it is the default button (the button that will be clicked when you press enter).

<Trigger Property="IsDefaulted" Value="True">
   <Setter TargetName="border" Property="BorderBrush" Value="#FF282828" />
</Trigger>
<Trigger Property="IsFocused" Value="True">
   <Setter TargetName="border" Property="BorderBrush" Value="#FF282828" />
</Trigger>

Finally, the when the button is disabled, we gray out the text and wash out the background a little by reducing its opacity.

<Trigger Property="IsEnabled" Value="False">
       <Setter TargetName="border" Property="Opacity" Value="0.7" />
       <Setter Property="Foreground" Value="Gray" />
   </Trigger>
</ControlTemplate.Triggers>

Stage 4: Using the control

Now we are ready to use our control. All we have to do is set the Style property of our button. We can override any of the settings such as font size if we like. The button will automatically resize itself to fit the content.

<StackPanel HorizontalAlignment="Center">
<Button Style="{StaticResource OrangeButton}">Hello</Button>
<Button Style="{StaticResource OrangeButton}">World</Button>
<Button Style="{StaticResource OrangeButton}" FontSize="20">Big Button</Button>
<Button Style="{StaticResource OrangeButton}" IsDefault="True">Default</Button>
<Button Style="{StaticResource OrangeButton}" IsEnabled="False">Disabled</Button>
<Button Style="{StaticResource OrangeButton}" Width="70" Height="30">70 x 30</Button>
<TextBox />
<Button Style="{StaticResource OrangeButton}" Width="30" Height="30">
<Path Fill="Black" Data="M 3,3 l 9,9 l -9,9 Z" />
</Button>
</StackPanel>

Here's what it looks like:

image

Download an example XAML file here.

37 comments:

F Quednau said...

Nice work...only nag is that I have trouble seeing your code (this is firefox 2.0.0.6): It gets clipped by its container at the point where the right-hand side starts.

Peregrinati said...

Me too - I've noticed this code clipping on a lot of sites (including code project!), it's quite aggrevating.

Good code otherwise!

Mark H said...

sorry about the clipping. hopefully you can get at the code by viewing the page source. I'm hoping to migrate to another blog platform sometime this year

Linda Rawson said...

It looks like great work. Is there anyway to get the entire code piece? Like exactly what I would put in the resource section?

Anonymous said...

very well explained. this was the best explaination after reading three books on WPF.

thanks!!!!

Steve said...

why don't you just type gibberish on the internet. Oh wait, you have! Complete code snippet please.

Anonymous said...

To all the people who'se panties are in a knot over the clipping issue (yes, you Steve) appreciate the fact that you have a code snippet that works. It's obviously not the authors fault. Go click view source you fool.

¡El Perro! said...

Where is the download link to get a visual studio project with this example????

Mark H said...

hi el perro,
I'm afraid there isn't a download link. I did this in XAML Pad.

Evan said...

This is wonderful, useful, and better still, I now know how to do it myself. Thanks!

For kicks, I added a trigger for when the button is disabled.

(To the complainers - the code is usable as-is. All you need to do is </end> a few tags, bung it into a resource dictionary, and tell App.xaml about the dictionary.)

Anonymous said...

It is not at all working. It was just a time weast from some fool.

Anonymous said...
This comment has been removed by a blog administrator.
wducklow said...

Hey, great example.

The one thing that is giving me problems is the mnemonics.

When I add "_Hello" to the content of the button the alt-H functionality is neither drawn or works. Any ideas why?

Nick said...

Thanks for the post. I am changing few colors now and put it to work in real life. Good job.

Amar Syafiq said...

Hi mark,
instead of creating the custom button in a page can i create it in window as well? or use it in window?

Mark H said...

Hi Amar, of course, you can use it wherever you like

RRave said...

Dear Sir,

I hope you are doing well. I got this email address from one of your contribution web site. I have launched a web site www.codegain.com and it is basically aimed C#,JAVA,VB.NET,ASP.NET,AJAX,Sql Server,Oracle,WPF,WCF and etc resources, programming help, articles, code snippet, video demonstrations and problems solving support. I would like to invite you as an author and a supporter.
Looking forward to hearing from you and hope you will join with us soon.

Thank you
RRaveen
Founder CodeGain.com

Bernd said...

Thanks for sharing this. Regarding the style for the FocusVisual I was wondering why you put a Rectangle and a Border inside the Grid... removing the Border does not seem to change anything.
Cheers,
Bernd

Mark H said...

Hi Bernd,
I can't quite remember why I put it in. It was a long time ago! I think it was to do with making sure the focus rectangle was an appropriate size, but from what you are saying, perhaps it is no longer needed.

Anonymous said...

Hi,
what do i have to do if i want the button to be round? I tried to change the rectangle to ellipse in the "MyFocusVisual"-Style but that doesnt work. any ideas?

Timo said...

Hey,

good and easy to understand tutorial on creating and styling a control template in WPF. If you would like to take the design to another level see my Button Styling Tutorial (www.complified.com) ;-)

Mark H said...

Hi anonymous,
check out my post
here for a circular button template.

Timo, that's a really nice WPF styling tutorial, thanks for the link

Sheshu Panga said...

Hi Mark,
It's indeed a nice article, though very old but still applicable. I live in Bournemouth, UK and in my spare time I try to write some applications for my every day use. This article has helped me a lot - Thanks a lot. How do I follow your blog? I don't see any option!
Regards,
Sheshu -

Mark H said...

hi Sheshu,
I've added a subscribe link to the right, although your feed reader should be able to determine the RSS URL if you just give it the main site feed.

Anonymous said...

In your last button example you use
Path Fill="Black"

How can I change color from black to white on IsMouseOver trigger?

Thank you.

Mark H said...

hi anonymous,
I haven't worked out a way to make that happen. If I ever find out I'll update this post.
Mark

pawels blog said...

Hi! Thank your for this example! It's very nice.
I adopted your xaml-code to my
Application.Recources.
If I add the Buttons, as you described, in my MainWindow.xaml, i can used ist -> no problem.
My Problem is that I want to add the Button dynamically in my C# code.
E.G.: Button myButton = new Button();
But how can I add the Style of your Button to myButton?
Can you, or anyone else, give me a hint? :)

Thx!

celil aydin said...

Hi
Thank you for this very helpfull tutoroial. I have a questions regarding to templates in XAML.Let's assume that I created one button templated in Templates.xaml file and I would like to use that button in MyButtons.xaml file in the same project. How it should be referred in the second XAML file? In other words, is it possible to use templates in another XAML files?
Best regards.

Brad said...

Nice work. Do you know how to make a accelerator character work with this? I loose this feature when specify the button style.


Thanks,
Brad

victor cassel said...

Thank you! I used this to create a button with both image and text. It works great!

Clark Kent said...
This comment has been removed by a blog administrator.
Anonymous said...

How do you use a hotkey with these buttons, like Alt+H for the Hello button?
Thanks.

Mark H said...

@anonymous, try looking here:
http://stackoverflow.com/questions/2186738/wpf-button-mnemonic-does-not-work-if-placing-the-button-in-a-toolbar

LePanther said...

This is a great code! Thanks for sharing! :-)

Mel said...

If you're interested in enabling the AccessKey functionality in this template, meaning a Button with "_Cancel" as its content will display as "Cancel" and accept Alt+C as an activation event, then you simply need to add RecognizeAccessKey="True" to the ContentPresenter with Name="content".

Mark H said...

thanks Mel, that's a useful tip

Wolfgang U. said...

Thx - I like the example, modify the code for my uses and I am happy to figure it out, how to create a button with a template. Well - this is my first day in WPF ;)