Switch On The Code RSS Button - Click to Subscribe
Feb
20

Creating an XP Style WPF Button with Silverlight

Way back in October I posted a tutorial on how to use custom controls in Silverlight. In that post I used an example button that looks like the Windows XP WPF button. Since then a couple of people have expressed interest in how to build that button. This tutorial is going to show exactly that.



The first thing we need to do is figure out what a WPF button button looks like in Windows XP. All I did for this was create an application and launch it in XP, then take some screen shots. It turns out that the buttons have three basic states - up, over, and down. There's also a focused state, but Silverlight doesn't do well with focus so I ignored it. The image below contains each of the states that we have to reproduce in our button.


Before I dive into any code I need to complain about how much stuff Silverlight has removed from XAML (as compared to Windows Presentation Foundation). Most of my XAML experience comes from WPF, so what originally seemed obvious turned out to be much more difficult when I discovered Silverlight didn't support what I wanted to do. If this button were built under WPF, I believe the entire thing could have been made using XAML. However, to build this in Silverlight, I had to implement a lot using C#.

Creating a user control in Silverlight is pretty straight forward. I simply right-mouse clicked on my project and chose to add a new item. The dialog that is presented has an option for "Silverlight User Control". Just give it a name a click "Add".


The XAML


Since the XAML support in Silverlight is pretty limited, I ended up not using very much XAML code when creating my button. Every state in the XP button has a blue border, so that's the first thing we should create.

<Canvas xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="80"
        Height="30"
        x:Name="cnvButton"
       >

  <Rectangle StrokeThickness="1.5"
             x:Name="buttonRect"
             Width="80"
             Height="30"
             RadiusX="4"
             RadiusY="4">

    <Rectangle.Stroke>
      <LinearGradientBrush StartPoint="0,0"
                           EndPoint="0,1">

        <LinearGradientBrush.GradientStops>
          <GradientStop Color="#396d95" />
          <GradientStop Color="#3a6180" />
        </LinearGradientBrush.GradientStops>
      </LinearGradientBrush>
    </Rectangle.Stroke>
  </Rectangle>
</Canvas>

The Canvas is what Visual Studio automatically created when I added the user control. I thought 80x30 was a pretty good default size for my button. I then gave the canvas a name so I could refer to it the C# code. I know that seems like a lot of XAML just to make a blue rectangle, but if you look closely at the XP button, the border has a slight gradient between two shades of blue. The above XAML code should give you something that looks the following:


The next thing I put into XAML was the orange rectangle that appears when you mouse over the control.

<Canvas xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="80"
        Height="30"
        x:Name="cnvButton"
       >

  <Rectangle StrokeThickness="1.5"
             x:Name="buttonRect"
             Width="80"
             Height="30"
             RadiusX="4"
             RadiusY="4">

    <Rectangle.Stroke>
      <LinearGradientBrush StartPoint="0,0"
                           EndPoint="0,1">

        <LinearGradientBrush.GradientStops>
          <GradientStop Color="#396d95" />
          <GradientStop Color="#3a6180" />
        </LinearGradientBrush.GradientStops>
      </LinearGradientBrush>
    </Rectangle.Stroke>
  </Rectangle>
  <Rectangle StrokeThickness="1.5"
             Width="77"
             Height="27"
             Canvas.Top="1.5"
             Canvas.Left="1.5"
             RadiusX="4"
             RadiusY="4"
             Visibility="Collapsed"
             x:Name="rectOver"
            >

    <Rectangle.Stroke>
      <LinearGradientBrush StartPoint="0,0"
                           EndPoint="0,1">

        <LinearGradientBrush.GradientStops>
          <GradientStop Color="#ffd076" Offset="0" />
          <GradientStop Color="#efa007" Offset="1" />
        </LinearGradientBrush.GradientStops>
      </LinearGradientBrush>
    </Rectangle.Stroke>
  </Rectangle>
</Canvas>

The orange rectangle is made exactly like the blue one. I just offset it a little and made it a little smaller. The orange rectangle isn't visible until the button is moused over, so I defaulted it to invisible. Normally I would have used Event Triggers to control the orange rectangle, but it doesn't appear that Silverlight supports triggers.

The last thing we need to add to our XAML code is a TextBlock to hold the button's text. With that, we have the entire XAML code for our XP Button.

<Canvas xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="80"
        Height="30"
        x:Name="cnvButton"
       >

  <Rectangle StrokeThickness="1.5"
             x:Name="buttonRect"
             Width="80"
             Height="30"
             RadiusX="4"
             RadiusY="4">

    <Rectangle.Stroke>
      <LinearGradientBrush StartPoint="0,0"
                           EndPoint="0,1">

        <LinearGradientBrush.GradientStops>
          <GradientStop Color="#396d95" />
          <GradientStop Color="#3a6180" />
        </LinearGradientBrush.GradientStops>
      </LinearGradientBrush>
    </Rectangle.Stroke>
  </Rectangle>
  <Rectangle StrokeThickness="1.5"
             Width="77"
             Height="27"
             Canvas.Top="1.5"
             Canvas.Left="1.5"
             RadiusX="4"
             RadiusY="4"
             Visibility="Collapsed"
             x:Name="rectOver"
            >

    <Rectangle.Stroke>
      <LinearGradientBrush StartPoint="0,0"
                           EndPoint="0,1">

        <LinearGradientBrush.GradientStops>
          <GradientStop Color="#ffd076" Offset="0" />
          <GradientStop Color="#efa007" Offset="1" />
        </LinearGradientBrush.GradientStops>
      </LinearGradientBrush>
    </Rectangle.Stroke>
  </Rectangle>
  <TextBlock Text="" x:Name="btnText"
             FontSize="11" Visibility="Collapsed" />

</Canvas>

The C# Code


Now that we've got the XAML out of the way, let's start looking at some code. I'll start by just getting my controls out of the XAML code and into my code-behind.

using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace XPButton
{
  public class XPButton : Control
  {
    private Rectangle rectOver;
    private Rectangle buttonRect;
    private Canvas cnvButton;
    private FrameworkElement xpButton;
    private LinearGradientBrush upBrush;
    private LinearGradientBrush downBrush;
    private TextBlock btnText;
    private bool mouseDown;

    public event EventHandler Click;

    private const double TEXT_PADDING = 3;

    public XPButton()
    {
      System.IO.Stream s =
        this.GetType().Assembly.GetManifestResourceStream(
        "XPButton.XPButton.xaml");

      this.xpButton =
        this.InitializeFromXaml(
        new System.IO.StreamReader(s).ReadToEnd());

      this.rectOver =
        this.xpButton.FindName("rectOver") as Rectangle;
      this.buttonRect =
        this.xpButton.FindName("buttonRect") as Rectangle;
      this.cnvButton =
        this.xpButton.FindName("cnvButton") as Canvas;
      this.btnText =
        this.xpButton.FindName("btnText") as TextBlock;

      this.mouseDown = false;
    }
}

The first thing I did was create some members to hold all of the elements from my XAML code. rectOver is the orange rectangle that appears when the mouse is over the buttons. buttonRect is the blue rectangle that also holds the two gradient backgrounds. cnvButton is the Canvas that holds my button controls. xpButton is returned when I initialize this control from my XAML code. I set and use this in the constructor to get the rest of my controls. upBrush holds the gradient when the button is not being pressed. downBrush holds the gradient when the button is being pressed. btnText is the TextBlock that holds the button's text. The mouseDown boolean is used to determine when to fire the Click event. I'll explain this in more detail later. Lastly I have the actual Click event and a constant to hold how much padding I want for the button's text.

All the constructor does is find each element in the XAML and set them to each of my private member variables. I also initialize the mouseDown boolean to false

Let's move on with something easy. Let's build the gradients for the button's background. From my example screen shots above, you can see there are only two gradients - one when the button is not pressed and another for when it is pressed. If this were WPF, I would have created these as resources in the XAML code, but it seems that Silverlight only supports Storyboards as resources.

using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace XPButton
{
  public class XPButton : Control
  {
    private Rectangle rectOver;
    private Rectangle buttonRect;
    private Canvas cnvButton;
    private FrameworkElement xpButton;
    private LinearGradientBrush upBrush;
    private LinearGradientBrush downBrush;
    private TextBlock btnText;
    private bool mouseDown;

    public event EventHandler Click;

    private const double TEXT_PADDING = 3;

    public XPButton()
    {
      System.IO.Stream s =
        this.GetType().Assembly.GetManifestResourceStream(
        "XPButton.XPButton.xaml");

      this.xpButton =
        this.InitializeFromXaml(
        new System.IO.StreamReader(s).ReadToEnd());

      this.rectOver =
        this.xpButton.FindName("rectOver") as Rectangle;
      this.buttonRect =
        this.xpButton.FindName("buttonRect") as Rectangle;
      this.cnvButton =
        this.xpButton.FindName("cnvButton") as Canvas;
      this.btnText =
        this.xpButton.FindName("btnText") as TextBlock;

      this.mouseDown = false;

      //build the up background gradient brush
      this.upBrush = new LinearGradientBrush();
      this.upBrush.StartPoint = new Point(0, 0);
      this.upBrush.EndPoint = new Point(0, 1);
      GradientStop gStop1 = new GradientStop();
      GradientStop gStop2 = new GradientStop();
      gStop1.Color = Color.FromRgb(255, 251, 255);
      gStop2.Color = Color.FromRgb(206, 207, 222);
      gStop1.Offset = 0;
      gStop2.Offset = 1;
      this.upBrush.GradientStops.Add(gStop1);
      this.upBrush.GradientStops.Add(gStop2);

      //build the down background gradient brush
      this.downBrush = new LinearGradientBrush();
      this.downBrush.StartPoint = new Point(0, 0);
      this.downBrush.EndPoint = new Point(0, 1);
      gStop1 = new GradientStop();
      gStop2 = new GradientStop();
      gStop1.Color = Color.FromRgb(189, 186, 206);
      gStop2.Color = Color.FromRgb(255, 255, 255);
      gStop1.Offset = 0;
      gStop2.Offset = 1;
      this.downBrush.GradientStops.Add(gStop1);
      this.downBrush.GradientStops.Add(gStop2);

      this.buttonRect.Fill = this.upBrush;
    }
}

So here I've added the code required to build the two gradient brushes. Since the button defaults to an 'up' state, I set the Fill property of buttonRect to upBrush. Now if you compile and run this thing, you'll get something that looks like this:


Now we need to make the background switch between the gradients when the mouse is down versus up. To do this, we first need to add some events to our constructor.

this.MouseLeftButtonUp += new MouseEventHandler(XPButton_MouseLeftButtonUp);
this.MouseLeftButtonDown += new MouseEventHandler(XPButton_MouseLeftButtonDown);
this.MouseLeave += new EventHandler(XPButton_MouseLeave);

And now we just create the handlers for each of these events.

void XPButton_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
  this.buttonRect.Fill = this.upBrush;
}

void XPButton_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
  this.buttonRect.Fill = this.downBrush;
}

void XPButton_MouseLeave(object sender, EventArgs e)
{
  this.buttonRect.Fill = this.upBrush;
}

Whenever the mouse button is up, this gradient is switched back to upBrush. Whenever the mouse button is down, the gradient will be downBrush. We also need to reset the gradient back to the up state whenever the mouse leaves the control.

The background will now switch when the user presses the mouse button. The next thing we need to do is have the orange rectangle appear whenever the user mouses over the control. We need to add one more event to make this happen - MouseEnter.

this.MouseLeftButtonUp += new MouseEventHandler(XPButton_MouseLeftButtonUp);
this.MouseLeftButtonDown += new MouseEventHandler(XPButton_MouseLeftButtonDown);
this.MouseLeave += new EventHandler(XPButton_MouseLeave);
this.MouseEnter += new MouseEventHandler(XPButton_MouseEnter);

Not only do we have to add a method to handle that event, we'll also need to modify our existing handlers for the orange rectangle.

void XPButton_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
  this.buttonRect.Fill = this.upBrush;
  this.rectOver.Visibility = Visibility.Visible;
}

void XPButton_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
  this.buttonRect.Fill = this.downBrush;
  this.rectOver.Visibility = Visibility.Collapsed;
}

void XPButton_MouseLeave(object sender, EventArgs e)
{
  this.buttonRect.Fill = this.upBrush;
  this.rectOver.Visibility = Visibility.Collapsed;
}

void XPButton_MouseEnter(object sender, MouseEventArgs e)
{
  this.rectOver.Visibility = Visibility.Visible;
}

The obvious thing to do is make the orange rectangle visible when the mouse enters the control, and invisible when the mouse leaves. In XP, the orange rectangle also disappears when the user presses the mouse - which means it should reappear when the user releases the mouse button.

Now we've got something that looks pretty close to the final product. Try it out for yourself.


The next thing we need to do it make it so the button can be resized. I tried to override the control's Width and Height properties, but for some reason they simply weren't being called when I set them using XAML. Therefore, I had to add new properties called ButtonWidth and ButtonHeight.

public double ButtonWidth
{
  get { return this.Width; }
  set
  {
    this.Width = value;
    this.UpdateButton();
  }
}

public double ButtonHeight
{
  get { return this.Height; }
  set
  {
    this.Height = value;
    this.UpdateButton();
  }
}

Just like WPF, pixels don't mean much anymore, so positioning and sizing are all done using doubles. The first thing I do in each property is set the Height and Width properties of the control. I then call a function called UpdateButton which resizing all the rectangles to the new size. This function will include other things later.

private void UpdateButton()
{
  this.cnvButton.Width = this.Width;
  this.rectOver.Width = this.Width - 3;
  this.buttonRect.Width = this.Width;
  this.cnvButton.Height = this.Height;
  this.rectOver.Height = this.Height - 3;
  this.buttonRect.Height = this.Height;
}

Now we can resize our button all we want.


I think we're ready to move on to the next piece of our button - the text. The first thing we need to do it add a couple of more properties. The first one is a string that sets what the text is and the other sets the size of the button's font. There's lots of other things that could be added to the button text, but I'll leave those up to you.

public string ButtonText
{
  get { return this.btnText.Text; }
  set
  {
    this.btnText.Text = value;
    this.UpdateButton();
  }
}

public double ButtonFontSize
{
  get { return this.btnText.FontSize; }
  set
  {
    this.btnText.FontSize = value;
    this.UpdateButton();
  }
}

These properties are pretty straight forward - they just set the corresponding property on our TextBlock that was created in our XAML code. We now need to modify the UpdateButton() function to keep the text centered.

private void UpdateButton()
{
  this.cnvButton.Width = this.Width;
  this.rectOver.Width = this.Width - 3;
  this.buttonRect.Width = this.Width;
  this.cnvButton.Height = this.Height;
  this.rectOver.Height = this.Height - 3;
  this.buttonRect.Height = this.Height;

  //center the text
  double textWidth = this.btnText.ActualWidth + TEXT_PADDING * 2;
  TranslateTransform tr = new TranslateTransform();
  if (textWidth <this.Width)
    tr.X = (this.Width - this.btnText.ActualWidth) / 2;
  else
    tr.X = TEXT_PADDING;
  tr.Y = (this.Height - this.btnText.ActualHeight) / 2;
  this.btnText.RenderTransform = tr;

  //set the clip bounds
  RectangleGeometry rect = new RectangleGeometry();
  rect.Rect = new Rect(0, 0, this.Width, this.Height);
  this.Clip = rect;
}

The first thing we need to do is center the text within the button. To do this I first need to get the width of the text inside of the TextBlock. This is easy to get thanks to the ActualWidth property. If the button is wider than the text, I simply center the text in the button. If the text is longer than the button, I left align the text. I move the text by using a TranslateTransform Render Transformation. The last thing I do is set the clip bounds of the button so the text doesn't spill outside of the button's edges.

Here you can see buttons with short text and long text.


Most of the time a developer isn't going to make the text longer than the button, but it's always a good idea to support things like that.

The very last thing we need to do it hook up a click event to this button. For some reason Microsoft got rid of the Click event on most controls - even in WPF. The closest thing the framework supports is MouseUp, but I only want to fire an event on a full click (i.e. the mouse goes down and up while inside the control). Do to this I need to keep a boolean of when the mouse goes down that I can check when the mouse goes up.

void XPButton_MouseLeftButtonUp(object sender,
  MouseEventArgs e)
{
  this.buttonRect.Fill = this.upBrush;
  this.rectOver.Visibility = Visibility.Visible;

  if (this.mouseDown)
  {
    if (this.Click != null)
      this.Click(this, new EventArgs());
  }
}

void XPButton_MouseLeftButtonDown(object sender,
  MouseEventArgs e)
{
  this.buttonRect.Fill = this.downBrush;
  this.rectOver.Visibility = Visibility.Collapsed;
  this.mouseDown = true;
}

void XPButton_MouseLeave(object sender, EventArgs e)
{
  this.rectOver.Visibility = Visibility.Collapsed;
  this.buttonRect.Fill = this.upBrush;
  this.mouseDown = false;
}

I modified some of my existing mouse events to toggle the boolean based on the mouse state. When the mouse is down, I set the boolean to true. When the mouse leaves the control, I set the boolea to false. When the mouse up event occurs, I check to see if the mouseDown boolean is set to true, if it is I fire the Click event.

That leaves us with the final code for the button:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace XPButton
{
  public class XPButton : Control
  {
    private Rectangle rectOver;
    private Rectangle buttonRect;
    private Canvas cnvButton;
    private FrameworkElement xpButton;
    private LinearGradientBrush upBrush;
    private LinearGradientBrush downBrush;
    private TextBlock btnText;
    private bool mouseDown;

    public event EventHandler Click;

    private const double TEXT_PADDING = 3;

    public double ButtonWidth
    {
      get { return this.Width; }
      set
      {
        this.Width = value;
        this.UpdateButton();
      }
    }

    public double ButtonHeight
    {
      get { return this.Height; }
      set
      {
        this.Height = value;
        this.UpdateButton();
      }
    }

    public string ButtonText
    {
      get { return this.btnText.Text; }
      set
      {
        this.btnText.Text = value;
        this.UpdateButton();
      }
    }

    public double ButtonFontSize
    {
      get { return this.btnText.FontSize; }
      set
      {
        this.btnText.FontSize = value;
        this.UpdateButton();
      }
    }

    public XPButton()
    {
      System.IO.Stream s =
        this.GetType().Assembly.GetManifestResourceStream(
        "XPButton.XPButton.xaml");

      this.xpButton =
        this.InitializeFromXaml(
        new System.IO.StreamReader(s).ReadToEnd());

      this.rectOver =
        this.xpButton.FindName("rectOver") as Rectangle;
      this.buttonRect =
        this.xpButton.FindName("buttonRect") as Rectangle;
      this.cnvButton =
        this.xpButton.FindName("cnvButton") as Canvas;
      this.btnText =
        this.xpButton.FindName("btnText") as TextBlock;

      this.mouseDown = false;

      //build the up background gradient brush
      this.upBrush = new LinearGradientBrush();
      this.upBrush.StartPoint = new Point(0, 0);
      this.upBrush.EndPoint = new Point(0, 1);
      GradientStop gStop1 = new GradientStop();
      GradientStop gStop2 = new GradientStop();
      gStop1.Color = Color.FromRgb(255, 251, 255);
      gStop2.Color = Color.FromRgb(206, 207, 222);
      gStop1.Offset = 0;
      gStop2.Offset = 1;
      this.upBrush.GradientStops.Add(gStop1);
      this.upBrush.GradientStops.Add(gStop2);

      //build the down background gradient brush
      this.downBrush = new LinearGradientBrush();
      this.downBrush.StartPoint = new Point(0, 0);
      this.downBrush.EndPoint = new Point(0, 1);
      gStop1 = new GradientStop();
      gStop2 = new GradientStop();
      gStop1.Color = Color.FromRgb(189, 186, 206);
      gStop2.Color = Color.FromRgb(255, 255, 255);
      gStop1.Offset = 0;
      gStop2.Offset = 1;
      this.downBrush.GradientStops.Add(gStop1);
      this.downBrush.GradientStops.Add(gStop2);

      this.buttonRect.Fill = this.upBrush;

      this.MouseEnter +=
        new MouseEventHandler(XPButton_MouseEnter);
      this.MouseLeave +=
        new EventHandler(XPButton_MouseLeave);
      this.MouseLeftButtonDown +=
        new MouseEventHandler(XPButton_MouseLeftButtonDown);
      this.MouseLeftButtonUp +=
        new MouseEventHandler(XPButton_MouseLeftButtonUp);
    }

    private void UpdateButton()
    {
      this.cnvButton.Width = this.Width;
      this.rectOver.Width = this.Width - 3;
      this.buttonRect.Width = this.Width;
      this.cnvButton.Height = this.Height;
      this.rectOver.Height = this.Height - 3;
      this.buttonRect.Height = this.Height;

      //center the text
      double textWidth = this.btnText.ActualWidth +
        TEXT_PADDING * 2;
      TranslateTransform tr = new TranslateTransform();
      if (textWidth <this.Width)
        tr.X = (this.Width - this.btnText.ActualWidth) / 2;
      else
        tr.X = TEXT_PADDING;
      tr.Y = (this.Height - this.btnText.ActualHeight) / 2;
      this.btnText.RenderTransform = tr;

      //set the clip bounds
      RectangleGeometry rect = new RectangleGeometry();
      rect.Rect = new Rect(0, 0, this.Width, this.Height);
      this.Clip = rect;
    }

    void XPButton_MouseLeftButtonUp(object sender,
      MouseEventArgs e)
    {
      this.buttonRect.Fill = this.upBrush;
      this.rectOver.Visibility = Visibility.Visible;

      if (this.mouseDown)
      {
        if (this.Click != null)
          this.Click(this, new EventArgs());
      }
    }

    void XPButton_MouseLeftButtonDown(object sender,
      MouseEventArgs e)
    {
      this.buttonRect.Fill = this.downBrush;
      this.rectOver.Visibility = Visibility.Collapsed;
      this.mouseDown = true;
    }

    void XPButton_MouseLeave(object sender, EventArgs e)
    {
      this.rectOver.Visibility = Visibility.Collapsed;
      this.buttonRect.Fill = this.upBrush;
      this.mouseDown = false;
    }

    void XPButton_MouseEnter(object sender, MouseEventArgs e)
    {
      this.rectOver.Visibility = Visibility.Visible;
    }
  }
}

I hope this tutorial does a good job clarifying how I created the button used in my previous tutorial. If there is anything that needs more explanation, just leave a comment and I'll answer any questions you may have. And even though we've never gotten around to stating it, all the code here on the blog is licensed under the BSD license, which means you can pretty do much whatever you want with it.

You can download a Visual Studio 2008 Solution with an example Silverlight application here.



Posted in Silverlight, All Tutorials by The Reddest |

6 Responses

  1. Michael Sync Says:

    hey man, that’s great tutorial that I was waiting for.. Thanks a lot for that.

  2. Michael Washington Says:

    Thi sis very nice.

  3. Luke Foust Says:

    Keep up the great work on these tutorials. These are amazing!

  4. Bluearc Says:

    a good one… keep it up.. and keep helping us :)

  5. M. Zain Ul Abedin Says:

    this is vary nice tutorial, i have learned a lot from this
    great work
    thanks

  6. San Says:

    Thanks for all the great tutorials. ^_^

    There is one minor UI question, the mouseDown is set to false on MouseLeave event, but if I keep the mouse button down when leaving it and then enter again (the mouse button is still down), how to make the button in DOWN state instead of UP state.