Introduction
One of the more common WPF chores is restyling a control. In most cases, a simple style or, at most, a control template, will do. But in some cases, the restyling is so extensive that a custom control is needed. An example of such a situation is creating Outlook-style task buttons.
In the past, I created these buttons using a control template with a ContentPresenter
. It did the job, but for reasons that I will explain below, I decided that a custom control would work better. That's what led to this article.
The article is divided into two parts:
- Part 1: In this part, we will create a WPF control template for the button. We will do most of our work in Expression Blend, although the template can be hand-coded in Visual Studio.
- Part 2: In Part 2, we will wrap the control template in a custom control. We will do most of our work in Visual Studio.
As always, I also have a second purpose in publishing this article: I would like to get peer review from the CodeProject community. If you have corrections or suggestions for improvements, I invite your comments.
Step One: Problem Analysis
What we want to do is really pretty simple: we want to create a button that looks and acts like an Outlook 2010 task button. So, let's start by taking a look at the button we are going to emulate.
For years, Outlook has had a set of buttons in its lower-left corner that configures the program to perform the various tasks of which it is capable:
The look and feel of the buttons has been updated for Outlook 2010, although the buttons' functionality remains unchanged from previous versions. The first thing to notice about the buttons is that they are 28 pixels high and contain an image and text. The image is 24 x 24, and the text is Segoe UI 9-point bold. The image has a 4-point left margin, and the text has a 6-point left margin. In addition, the buttons have four states:
- Default: The button is not selected, and it blends into the background.
- Selected: A button that has selected has a border, a drop shadow, and some glass effects.
- MouseOver: When the mouse is over a button, it appears much as if it is selected. However, the glass effects are a bit more intense that for the 'selected' state.
- Pressed: When a button is pressed, it turns a bit darker and a drop shadow appears inside the button.
So, these are the four states that we will need to model in our button. We will implement them through triggers. In addition, when we select a button in a group, all other buttons in the group should be deselected.
Step Two: Initial Design
Now that we have a general idea of what we are going to create, let's start working on how to implement the button. You probably noticed above that a group of task buttons behaves like a group of radio buttons - select one, and all others deselect. So our first design choice is easy - we will base our task button of a WPF radio button. But how do we implement our custom look and feel?
The simplest choice is a control template in a resource dictionary. That's always your best bet, if it will work in your situation, and that makes it a good starting point. If you aren't familiar with control templates, this is a good point to take a detour and familiarize yourself with them. We will assume from this point that you have a general understanding of what control templates are all about.
ContentPresenter for Variable Content
So the first question is: Can we get this job done with no more than a control template? We can, so long as each button can have its own icon and text. We can provide that flexibility by setting up our control template with a ContentPresenter
control. Rather than adding an image control and a text block to the control template, we add a ContentPresenter
, which acts as a placeholder in a control template. We can fill that place in a templated control by placing the image control and text block between the opening and closing tags of the templated button, rather than in the control template:
<RadioButton Template="TaskButton">
<StackPanel Orientation="Horizontal">
<Image Source="MyImage.png">
<TextBlock Text="My Task Button">
</StackPanel>
</ RadioButton>
The stack panel is considered the content of the RadioButton
control, since it falls between the opening and closing tags of the control. A ContentPresenter
in a control template will read this content and display it. A ContentPresenter
is the quick-and-dirty way to enable a control template to handle variable content, and in most cases, it works just fine.
Use a Custom Control to Enforce Standards
Content presenters are great, but their flexibility creates a problem: a developer can put pretty much anything they want in a content presenter. If we were creating a general purpose button, the ContentPresenter
would fit the bill nicely, but in fact, we are creating a special purpose button - an Outlook 2010 task button. Our button will always have an icon and text, and nothing else. The image needs to be 24 x 24, the text needs to be Segoe UI 9-point bold, and the image and text margins need to be the same for all buttons.
The only way we can enforce these standards is to move the button's content into the control template. That will ensure that any control that implements the template will have the same appearance. But it brings us back to our original problem: each button will use a different image and text, so we need to be able to specify that content from outside the template, without changing the appearance of the control.
WPF allows us to data-bind properties inside a control template to properties of the templated control. For example, let's say we are using a Rectangle
to provide the background for a templated control. We can bind the Rectangle
's Fill
property to the Background
property of the templated control, like this:
<Rectangle Fill="{TemplateBinding Background}" />
That works great in cases where the templated control has a convenient property, like Background
, that we can hook into. But for our task button, we need properties to provide a file path to the image we will use as our button icon, and text for the button, as well. A radio button has neither an ImagePath
property nor a Text
property. So, we are just going to have to create these properties.
And that's where a custom control comes in. A custom control will allow us to bundle up our control template with a C# class that will provide the properties the template needs. To do that, we will derive our custom control from the RadioButton
class.
So, we know we are going to use a custom control derived from the RadioButton
control to encapsulate a control template that will emulate the look, feel, and behavior of an Outlook 2010 task button. Let's begin our implementation with the control template. In Part 2, we will wrap the control template in a custom control.
Step Three: Create the Control Template
You can create a control template in either Expression Blend or in Visual Studio. Blend makes the job much easier; it has a full-featured design environment that nearly eliminates hand-coding XAML. It is a great way to generate a user interface quickly and efficiently. Note that you can get the job done in Visual Studio, but its XAML design environment is crude by comparison, and most of the XAML will need to be hand-coded. We won't cover the hand-coding in this article, but you can examine the XAML in the completed solution to see what you need.
We will use Blend 3 to design our control template. If you don't have Blend, you can download a 90-day trial from Microsoft. If you aren't familiar with Blend, you will need to take another detour and learn the basics. We will assume that you have a basic familiarity with the program from this point on.
Before we create the control template, there is one more side trip you will need to take. An Outlook 2010 task button is a version of what is known as a 'glass' button, and the control template will be built around managing glass-button effects. So, you will need to understand how to create a glass button. We aren't going to cover that technique in detail in this article, because there is a great tutorial by Martin Grayson on the subject. Once you go through it, you will understand not only glass effects, but the process of creating and using triggers to control those effects, and the process for creating and modifying control templates, as well. Note that the Grayson tutorial covers Vista-style black glass buttons. But as you will see below, Outlook 2010 task buttons are really just a variation of the same technique. Once you have been through the tutorial, you will be able to create pretty much any kind of glass button you will need.
Create a Control Template
We are going to prototype our control template in an Expression Blend project. We will do everything in the project's main window. To begin the process, create a new project in Blend and add a radio button to the main window. Right-click the button in the Objects and Timeline pane on the left, and select Edit Template > Create Empty from the context menu. This will create a new, empty control template for the button, with a grid control as the layout root. Name the grid LayoutRoot
. The grid will have only one row and one column. As you will see below, we are going to use grids in several places to create layers that we can turn on and off as needed, much as we would in Photoshop or Expression Design.
Add Content Controls
Now, create a stack panel in the LayoutRoot
grid. The stack panel should have a horizontal orientation. Add an image control and a text block to the stack panel. The image control should be set to 24 x 24 with a 4-pixel left margin, and its Source
property should be set to a sample image in your project:
<Image Height="24" HorizontalAlignment="Left"
Margin="4,0,0,0" Source="calendar.png"/>
Add a text block to the stack panel, and set its properties as follows:
<TextBlock Text="Calendar" HorizontalAlignment="Center"
VerticalAlignment="Center" FontFamily="Segoe UI" FontWeight="Bold"
Margin="6,0,0,0" Foreground="#FF1E395B" />
Note that we are starting out with hard-coded reference to a specific image and to specific text in our control template, as well as several other hard-coded properties. We are doing this to make it easier to work out the settings for our control template. In Part 2 of this article, we will bind all of these properties to properties of the custom control.
At this point, we have the default behavior of our custom control. It is borderless, and it blends into the background of the host window. But of course, the colors match only because we hard-coded them that way.
Organize the Visual Effects into Layers
We are going to have several different visual effects, each of which will have several elements. They are going to be a lot easier to work with if we organize them into layers. If you have ever used Photoshop or Expression Design, you are probably familiar with layers, and you know how useful they can be. And, you have probably noticed that Blend does not have a Layers feature. That's not a problem, though - we can implement a Layers feature using Grid
controls.
Remember that a grid can hold multiple controls. We normally use rows and columns in a grid to arrange controls on a design surface, but by default, new controls are stacked on top of each other in Row 0, Column 0. So, the trick we are going to use is this: the grids in our control will not be divided into rows and columns. Instead, each grid will have a single cell. That way, whatever we add to a grid will get stacked on top of whatever else we have added to it.
The LayoutRoot
grid will act as the master container for our layers. We will stack several other grids inside LayoutRoot
; each will stretch horizontally and vertically to fill LayoutRoot
. These grids will act as the layers of our application, and we will show and hide them to display the various effects that our task button will contain.
Each of the layer grids will contain several Rectangle
controls to create the effect we want to display. We will stack objects inside the layer grids to create an effect. As you will see in the effect for the Pressed state, we can produce some sophisticated results, like an inner drop shadow, that WPF does not natively support.
My initial thought was to create a layer for each state we want to model; Default, MouseOver, Pressed, and Selected. However, I discovered pretty quickly that certain elements, like the button border, were used by multiple states. So, I refactored my design to decompose the button to its functional elements:
LayoutRoot
: This grid provides the background of the button for the Default state, in addition to acting as the Layers container.
BorderGrid
: This layer contains the button border, consisting of two Rectangle
objects and a drop shadow effect. This layer is used in the MouseOver and Selected states.
IsPressedGrid
: This layer provides the background for the Pressed state. It contains several Rectangle
objects that we will use to emulate an inner drop shadow.
IsCheckedGlow
: This layer is made up of a Rectangle
object that provides a glow to the button for the Selected state.
IsMouseOverGlow
: This layer is made up of a Rectangle
object that provides a glow to the button for the MouseOver state.
Shine
: This layer is made up of a Rectangle
object that provides a shine effect to the upper part of the button. It is used in the MouseOver and Pressed states.
You can see in the Expression Blend Objects and Timeline panel how the various layers are stacked one on top of another in the LayoutRoot
grid:
The order of the objects is important - objects lower in the list are placed on top of objects higher in the list and will hide those objects when shown.
Creating the Button Border
The LayoutRoot
layer provides the default background for the button. Ultimately, we will bind the color to the Background
property of the host window. For now, I simply used Blend's Eyedropper tool to copy the background of the Outlook 2010 window.
The BorderGrid
layer contains the border effects that the button displays in its MouseOver and Selected states. The border consists of outer and inner strokes, and a drop shadow. Later, we will bind the stroke color properties, but for now, I used the Eyedropper to grab them from Outlook 2010. You can see the settings for the various elements in the control template project.
Note that the opacity for BorderGrid
is set to zero. That means the button border is invisible when the button is in its Default state. Later, we will use triggers to display the grid when the button is in its MouseOver and Pressed states. If you want to see what the border looks like, set the opacity value to 100%. Be sure to reset the opacity value to 0% when you are done, since the grid is supposed to be invisible in the Default state.
Creating Glass Effects
The Outlook 2010 task button is a bit of an evolution from the 'classic' Vista glass button, but it is built the same way. My implementation retains a faint shine at the top of the button, and I use two glows for the bottom of the button. The first is a strong glow that is used for the MouseOver state, and the second is a weaker glow for the Selected state. That way, the glow will intensify as the mouse passes over a selected button.
As with the border, the shine and glow effects have opacities of 0%, and we will use triggers later to change these settings to display the effects. If you play with the opacity values, remember to reset them to 0% when you are done.
Creating the Pressed Button Effect
The pressed button has a darker background than in other states, and it has an inside drop shadow. WPF does not provide an inner drop shadow, so we emulate the effect with three Rectangle
objects. The Rectangle
background and stroke colors will be data-bound later; as with the other elements, I grabbed values from Outlook 2010 for now, using the Eyedropper tool.
Note the margins on the drop-shadow Rectangle
objects. The top and left values increase from 1 to 3 for the rectangles, but the right and bottom values are all 0. This causes the right and bottom portion of the Rectangle
stroke to be hidden under the BorderRect
, which is what creates the drop-shadow effect. Otherwise, we would simply have a faked square gradient. As with the other non-default states, the Opacity
of the IsPressedGrid
is set to zero, so it is invisible until we need it.
Implementing Button States
There are a couple of ways to implement state in a control template. WPF has added a Visual State Manager that is very cool, but our button states are pretty simple, so we are going to use WPF's tried-and-true Trigger mechanism.
WPF's triggers are linked to various properties that define states for WPF controls, such as IsMouseOver
, IsPressed
, and IsChecked
. When one of these properties changes, WPF can invoke markup that changes settings for specified controls. In our case, we are going to use triggers to change the opacity values of the grids to display various effects. If you take a look at the Grayson Glass Button tutorial, you will get a pretty good introduction to triggers. So, from this point, we will assume you are generally familiar with what they are, how they work, and how to create them.
You can examine the triggers in the control template in the Triggers panel:
We are looking at the IsMouseOver trigger. It is activated when the button's IsPressed
property becomes true, and it is automatically deactivated when the property becomes false. When the trigger is activated, WPF will process the markup in the MouseOverOn storyboard, and when it is deactivated, WPF will process the markup in the MouseOverOff storyboard.
Storyboards are selected in the Objects and Timeline panel:
When you select a storyboard, a timeline opens up next to the object list. You use the time line to alter object properties when the storyboard executes. You can see that in the MouseOverOn storyboard, we modified the following controls, by changing their opacities to 100%:
BorderGrid
IsMouseOverGlow
Shine
Note the vertical gold line at 0:00.300. It means that each control will transition from its default state (0% opacity) to its trigger state (100% opacity) over 3/10the of a second, rather than instantaneously. That's what gives a mouse-over a nice, animated effect.
The other storyboards work pretty much the same way. Since we have organized the effects into functional layers, the storyboards by and large simply change opacity values from 0% to 100% and back. You can see the property changes made by each trigger by examining the XAML for the control template.
Conclusion
That pretty much wraps up Part Ones. We have created a working control template. However, it isn't terribly useful yet, since all of its colors are hard-coded into the template. In Part Two, we will refactor the template by wrapping it in a custom control and binding the color properties to the custom control.
History