Simulate 3D Buttons on Windows Phone


--Copyright 2010 Travis Feirtag


Introduction

I personally don't think a rectangle is a simple, elegant button. That's what they called it at Mix10 when they were talking about the Metro theme on Windows Phone. I agree that a rectangle is simple but I would never call it elegant (unless of course I was a designer type.) I wanted a nice 3D looking button to use in my Silverlight apps on Windows Phone. Here is my simple, elegant solution :)


Download SE3DButton Source Files

Download SE3DKeypadBeta Source Files


It's an Ellipse

First thing we need is an Ellipse. Okay, it's actually the second thing we need. The first thing we need is to create a Windows Phone Silverlight project. I'll call it SE3DButton. In the "ContentGrid" we're going to add an Ellipse.

<Grid x:Name="ContentGrid" Grid.Row="1">
    <Ellipse Name="elButton" Width="200" Height="200" Fill="Blue" Stroke="White" StrokeThickness="15">

    </Ellipse>
</Grid>

It should look like this.

Now we need to add a nice RadialGradientBrush onto the Ellipse.Fill.

<Grid x:Name="ContentGrid" Grid.Row="1">
    <Ellipse Name="elButton" Width="200" Height="200" Stroke="White" StrokeThickness="15">
        <Ellipse.Fill>
            <RadialGradientBrush x:Name="radBrush"
                  GradientOrigin="0.35,0.35"
                  Center="0.5,0.5" RadiusX="0.75" RadiusY="0.75">
                <RadialGradientBrush.GradientStops>
                    <GradientStop Color="AliceBlue" Offset="0" />
                    <GradientStop Color="Green" Offset="1" />
                </RadialGradientBrush.GradientStops>
            </RadialGradientBrush>
        </Ellipse.Fill>
    </Ellipse>
</Grid>

This looks much better already. It has a 3D look to it just by adding the gradient brush. I've moved the GradientOrigin more towards the upper left of the Ellipse. This will make sense when we add our EventTriggers.


I'd also like to add a nice gradient on the Stroke as well to enhance the 3D appearance of the button. Now our XAML should look like this.

<Grid x:Name="ContentGrid" Grid.Row="1">
    <Ellipse Name="elButton" Width="200" Height="200" StrokeThickness="15">
        <Ellipse.Fill>
            <RadialGradientBrush x:Name="radBrush"
                  GradientOrigin="0.35,0.35"
                  Center="0.5,0.5" RadiusX="0.75" RadiusY="0.75">
                <RadialGradientBrush.GradientStops>
                    <GradientStop Color="AliceBlue" Offset="0" />
                    <GradientStop Color="Green" Offset="1" />
                </RadialGradientBrush.GradientStops>
            </RadialGradientBrush>
        </Ellipse.Fill>
        <Ellipse.Stroke>
            <RadialGradientBrush
                  GradientOrigin="0.25,0.25"
                  Center="0.25,0.25" RadiusX="1" RadiusY="1">
                <RadialGradientBrush.GradientStops>
                    <GradientStop Color="WhiteSmoke" Offset="0" />
                    <GradientStop Color="Gray" Offset="1" />
                </RadialGradientBrush.GradientStops>
            </RadialGradientBrush>
        </Ellipse.Stroke>
    </Ellipse>
</Grid>

Now the outside ring has a RadialGradientBrush which gives it a brushed metal look to it with a slight shine. It gives the button an overall appearance of a light source originating in the upper left corner of the screen.


Adding the Storyboard Animations

Now that we have the 3D look we need to add event triggers to the Ellipse to simulate the user pressing the button. This will be done using PointAnimations and DoubleAnimations. However, at the time of this writing, I would normally create these EventTriggers in XAML. Unfortunately there seems to be a problem with the XAML parser in the Windows Phone Beta dev tools. So I will need to do some of this in code. Here's how I would do it in XAML, if the XAML parser was working properly. Incidentally, this XAML works correctly on the desktop.

THIS APPROACH CURRENTLY DOESN'T WORK ON Windows Phone Beta DEV TOOLS - XAMLParseException. THIS **DOES** WORK ON DESKTOP.


<Ellipse.Triggers>
    <EventTrigger RoutedEvent="MouseLeftButtonDown" >
        <BeginStoryboard>
            <Storyboard>
                <PointAnimation Storyboard.TargetProperty="(RadialGradientBrush.GradientOrigin)"
                                Storyboard.TargetName="radBrush"
                                From="0.35,0.35" To="0.65,0.65" Duration="0:0:0" />

                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusX)"
                                 Storyboard.TargetName="radBrush"
                                 From="0.75" To="1" Duration="0:0:0" />
                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusY)"
                                 Storyboard.TargetName="radBrush"
                                 From="0.75" To="1" Duration="0:0:0" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
    <EventTrigger RoutedEvent="MouseLeftButtonUp" >
        <BeginStoryboard>
            <Storyboard>
                <PointAnimation Storyboard.TargetProperty="(RadialGradientBrush.GradientOrigin)"
                                Storyboard.TargetName="radBrush"
                                From="0.5,0.5" To="0.35,0.35" Duration="0:0:0" />
                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusX)"
                                 Storyboard.TargetName="radBrush"
                                 From="1.25" To="0.75" Duration="0:0:0" />
                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusY)"
                                 Storyboard.TargetName="radBrush"
                                 From="1.25" To="0.75" Duration="0:0:0" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Ellipse.Triggers>

The alternative approach is to add the storyboards as resources to the LayoutGrid, load them in code and then hook the MouseLeftButton events of the Ellipse to run the animations. It's actually quite simple.

First we add the storyboards in XAML. We need one storyboard for the MouseLeftButtonDown event and one for the MouseLeftButtonUp event. We use a PointAnimation to move the GradientOrigin of the RadialGradientBrush. And we will use two DoubleAnimations to adjust the RadiusX and RadiusY properties of the RadialGradientBrush. I've set the Duration to zero time some that it happens instantaneously. I've tried adding a duration but it didn't look realistic enough to me.

<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneBackgroundBrush}">
    <Grid.Resources>
        <Storyboard x:Name="MouseDownStoryboard">
            <PointAnimation Storyboard.TargetProperty="(RadialGradientBrush.GradientOrigin)"
                                        Storyboard.TargetName="radBrush"
                                        From="0.35,0.35" To="0.65,0.65" Duration="0:0:0" />

            <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusX)"
                                         Storyboard.TargetName="radBrush"
                                         From="0.75" To="1" Duration="0:0:0" />
            <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusY)"
                                         Storyboard.TargetName="radBrush"
                                         From="0.75" To="1" Duration="0:0:0" />
        </Storyboard>
        <Storyboard x:Name="MouseUpStoryboard">
            <PointAnimation Storyboard.TargetProperty="(RadialGradientBrush.GradientOrigin)"
                                        Storyboard.TargetName="radBrush"
                                        From="0.5,0.5" To="0.35,0.35" Duration="0:0:0" />
            <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusX)"
                                         Storyboard.TargetName="radBrush"
                                         From="1.25" To="0.75" Duration="0:0:0" />
            <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusY)"
                                         Storyboard.TargetName="radBrush"
                                         From="1.25" To="0.75" Duration="0:0:0" />
        </Storyboard>
    </Grid.Resources>

In the MainPage.xaml.cs file, we need to add two private Storyboard members to the page. We then hook the Loaded event of the page and find the storyboard resources in the LayoutGrid. Finally, we simply hook the MouseLeftButtonDown and MouseLeftButtonUp events of the Ellipse and run the animations when the user presses the button.

public partial class MainPage : PhoneApplicationPage
{
    private Storyboard _sbMouseDown;
    private Storyboard _sbMouseUp;

    public MainPage()
    {
        InitializeComponent();

        SupportedOrientations = SupportedPageOrientation.Portrait | SupportedPageOrientation.Landscape;

        this.Loaded += new RoutedEventHandler(MainPage_Loaded);
    }

    void MainPage_Loaded(object sender, RoutedEventArgs routedEventArgs)
    {
        this._sbMouseDown = (Storyboard)this.LayoutRoot.FindName("MouseDownStoryboard");
        this._sbMouseUp = (Storyboard)this.LayoutRoot.FindName("MouseUpStoryboard");
    }

    private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        this._sbMouseDown.Begin();
    }

    private void Button_MouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        this._sbMouseUp.Begin();
    }
}

It's just that easy. (It would be easier if the blasted XAML parser was working) Now run the application and click on the button. You will see the 3D simulated button react and make it appear that the Ellipse has been pressed by moving the shading. It makes it look like the dome of the button has inverted by changing the shading.

Adding a TextBlock

We should probably put some text on the button. We'll start by adding a TextBlock onto the page and centering it horizontally and vertically. The TextBlock is a UIElement so it will also need to hook the Mouse events. We can simply hook the existing Mouse events and it all magically works. You could probably add a color change to the text to add to the effect. I'll leave that up to the reader to add that functionality.

<Grid x:Name="ContentGrid" Grid.Row="1">
    <Ellipse Name="elButton" Width="200" Height="200" StrokeThickness="15"
            MouseLeftButtonDown="Button_MouseLeftButtonDown"
            MouseLeftButtonUp="Button_MouseLeftButtonUp">
        <Ellipse.Fill>
            <RadialGradientBrush x:Name="radBrush"
                  GradientOrigin="0.35,0.35"
                  Center="0.5,0.5" RadiusX="0.75" RadiusY="0.75">
                <RadialGradientBrush.GradientStops>
                    <GradientStop Color="AliceBlue" Offset="0" />
                    <GradientStop Color="Green" Offset="1" />
                </RadialGradientBrush.GradientStops>
            </RadialGradientBrush>
        </Ellipse.Fill>
        <Ellipse.Stroke>
            <RadialGradientBrush
                  GradientOrigin="0.25,0.25"
                  Center="0.25,0.25" RadiusX="1" RadiusY="1">
                <RadialGradientBrush.GradientStops>
                    <GradientStop Color="WhiteSmoke" Offset="0" />
                    <GradientStop Color="Gray" Offset="1" />
                </RadialGradientBrush.GradientStops>
            </RadialGradientBrush>
        </Ellipse.Stroke>
    </Ellipse>
    <TextBlock Name="txtName" Text="Press" FontSize="36" Foreground="Black"
                   HorizontalAlignment="Center" VerticalAlignment="Center"
                   MouseLeftButtonDown="Button_MouseLeftButtonDown"
                   MouseLeftButtonUp="Button_MouseLeftButtonUp" />
</Grid>

Creating a Keypad

The previous paragraphs show you how to create a single button on the page. But what if we want to use the button many times. Theoretically, we could copy - paste the same code over and over again. But as we all know, that isn't very good coding practice. We want to create a reusable button control. So that's what we'll do. I'll start by creating a project called SE3DKeypad. And we'll add a ButtonCtrl.xaml user control to the project. Right click the project and select Add -> New Item... Select Silverlight for Windows Phone for Installed Templates on the left and select Windows Phone User Control and name it ButtonCtrl.xaml We're just going to add the same XAML and code that we used in the previous project. One change we will make is that we'll make the Width and Height set to Auto. That way you can change the Width and Height of the UserControl and the Ellipse will change with it. Very cool because you can make it an oval button, if you like. Here's the completed XAML.

<UserControl x:Class="SE3DKeypad.ButtonCtrl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="150" d:DesignWidth="150">

    <Grid x:Name="LayoutRoot" Background="#FF1F1F1F">
        <Grid.Resources>
            <Storyboard x:Name="MouseDownStoryboard">
                <PointAnimation Storyboard.TargetProperty="(RadialGradientBrush.GradientOrigin)"
                                            Storyboard.TargetName="radBrush"
                                            From="0.35,0.35" To="0.65,0.65" Duration="0:0:0" />

                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusX)"
                                             Storyboard.TargetName="radBrush"
                                             From="0.75" To="1" Duration="0:0:0" />
                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusY)"
                                             Storyboard.TargetName="radBrush"
                                             From="0.75" To="1" Duration="0:0:0" />
            </Storyboard>
            <Storyboard x:Name="MouseUpStoryboard">
                <PointAnimation Storyboard.TargetProperty="(RadialGradientBrush.GradientOrigin)"
                                            Storyboard.TargetName="radBrush"
                                            From="0.5,0.5" To="0.35,0.35" Duration="0:0:0" />
                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusX)"
                                             Storyboard.TargetName="radBrush"
                                             From="1.25" To="0.75" Duration="0:0:0" />
                <DoubleAnimation Storyboard.TargetProperty="(RadialGradientBrush.RadiusY)"
                                             Storyboard.TargetName="radBrush"
                                             From="1.25" To="0.75" Duration="0:0:0" />
            </Storyboard>
        </Grid.Resources>
        <Ellipse Name="elButton" Width="Auto" Height="Auto" StrokeThickness="12"
                 MouseLeftButtonDown="Button_MouseLeftButtonDown"
                 MouseLeftButtonUp="Button_MouseLeftButtonUp">
            <Ellipse.Fill>
                <RadialGradientBrush x:Name="radBrush"
                      GradientOrigin="0.35,0.35"
                      Center="0.5,0.5" RadiusX="0.75" RadiusY="0.75">
                    <RadialGradientBrush.GradientStops>
                        <GradientStop Color="AliceBlue" Offset="0" />
                        <GradientStop Color="Green" Offset="1" />
                    </RadialGradientBrush.GradientStops>
                </RadialGradientBrush>
            </Ellipse.Fill>
            <Ellipse.Stroke>
                <RadialGradientBrush
                      GradientOrigin="0.25,0.25"
                      Center="0.25,0.25" RadiusX="1" RadiusY="1">
                    <RadialGradientBrush.GradientStops>
                        <GradientStop Color="WhiteSmoke" Offset="0" />
                        <GradientStop Color="Gray" Offset="1" />
                    </RadialGradientBrush.GradientStops>
                </RadialGradientBrush>
            </Ellipse.Stroke>
        </Ellipse>

        <TextBlock Name="txtName" Text="Press" FontSize="36" Foreground="Black"
                       HorizontalAlignment="Center" VerticalAlignment="Center"
                       MouseLeftButtonDown="Button_MouseLeftButtonDown"
                       MouseLeftButtonUp="Button_MouseLeftButtonUp" />

    </Grid>
</UserControl>

Here is the source code. I've added something to the code. We needed a property to be able to set the text of the button. You could also add another property to change the colors of the button as well. Again, I'll leave that to the reader to make the changes. We also need a Click event to occur when the user presses the button. So I've added an event handler for the main app to hook. The ButtonClick event will fire when the storyboard for the mouse down event is completed. I hooked the Completed event on the storyboard. If you'd rather have it fire on the completion of the mouse up storyboard, simply hook the completed event for _sbMouseUp instead.

public partial class ButtonCtrl : UserControl
{
    private Storyboard _sbMouseDown;
    private Storyboard _sbMouseUp;

    public ButtonCtrl()
    {
        InitializeComponent();

        this.Loaded += new RoutedEventHandler(ButtonCtrl_Loaded);
    }

    public string ButtonText
    {
        set
        {
            if (string.IsNullOrEmpty(value) == false)
                this.txtName.Text = value;
        }
        get { return this.txtName.Text; }
    }

    void ButtonCtrl_Loaded(object sender, RoutedEventArgs e)
    {
        this._sbMouseDown = (Storyboard)this.LayoutRoot.FindName("MouseDownStoryboard");
        this._sbMouseUp = (Storyboard)this.LayoutRoot.FindName("MouseUpStoryboard");

        this._sbMouseDown.Completed += new EventHandler(sbMouseDown_Completed);
    }

    private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        this._sbMouseDown.Begin();
    }

    private void Button_MouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        this._sbMouseUp.Begin();
    }

    public event EventHandler ButtonClick;

    public void OnButtonClick(Object objSender, EventArgs eventArgs)
    {
        if (ButtonClick != null)
            ButtonClick(objSender, eventArgs);
    }

    void sbMouseDown_Completed(object sender, EventArgs eventArgs)
    {
        OnButtonClick(this, new EventArgs());
    }
}

Here is the source code. I've added something to the code. We needed a property to be able to set the text of the button. You could also add another property to change the colors of the button as well. Again, I'll leave that to the reader to make the changes. We also need a Click event to occur when the user presses the button. So I've added an event handler for the main app to hook. The ButtonClick event will fire when the storyboard for the mouse down event is completed. I hooked the Completed event on the storyboard. If you'd rather have it fire on the completion of the mouse up storyboard, simply hook the completed event for _sbMouseUp instead.

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();

        SupportedOrientations = SupportedPageOrientation.Portrait | SupportedPageOrientation.Landscape;

        this.Loaded += new RoutedEventHandler(MainPage_Loaded);
    }

    void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        List<string> keyOrder = new List<string>() { "7", "8", "9", "4", "5", "6", "1", "2", "3", "*", "0", "#" };
        int i = 0;
        int nRow = 0;
        foreach (RowDefinition rowDef in this.ContentGrid.RowDefinitions)
        {
            int nCol = 0;
            foreach (ColumnDefinition colDef in this.ContentGrid.ColumnDefinitions)
            {
                ButtonCtrl buttonCtrl = new ButtonCtrl();
                buttonCtrl.ButtonText = keyOrder[i];
                buttonCtrl.Width = 130;
                buttonCtrl.Height = 130;
                buttonCtrl.SetValue(Grid.RowProperty, nRow);
                buttonCtrl.SetValue(Grid.ColumnProperty, nCol);

                buttonCtrl.ButtonClick += new EventHandler(ButtonCtrl_ButtonClick);

                this.ContentGrid.Children.Add(buttonCtrl);
                nCol++;
                i++;
            }
            nRow++;
        }
    }

    void ButtonCtrl_ButtonClick(object sender, EventArgs eventArgs)
    {
        if (sender is ButtonCtrl)
        {
            ButtonCtrl buttonCtrl = sender as ButtonCtrl;
            this.txtData.Text += buttonCtrl.ButtonText;
        }
    }
}

Don't forget that the ButtonCtrl will auto-size the Ellipse so you can make it oval by changing the Width or Height. I think that's pretty cool and makes it a versatile button control.


Conclusion

If you want something better than the boring old rectangular button, consider using an Ellipse and a RadialGradientBrush to spice up the look of your Windows Phone application.