Click here to Skip to main content
Click here to Skip to main content

Create a WPF Custom Control, Part 1

By , 31 Dec 2009
Rate this:
Please Sign up or sign in to vote.

ScreenShot.png

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:

OutlookTaskPanel.png

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:

ObjectsAndTimeline.png

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:

Triggers.png

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:

Storyboard.png

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

  • 2009-12-31: Published.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

David Veeneman
Software Developer (Senior) Foresight Systems
United States United States
David Veeneman is a financial planner and software developer. He is the author of "The Fortune in Your Future" (McGraw-Hill 1998). His company, Foresight Systems, develops planning and financial software.

Comments and Discussions

 
Questionvery nice job little bit change code and use all radioButton [modified] Pinmemberreptir4-Feb-14 14:45 
GeneralMy vote of 5 PinmemberJohn Patrick Francis17-Aug-13 16:55 
GeneralMy vote of 1 PinmemberIND0328-May-13 3:53 
GeneralMy vote of 5 PinmemberShahin Khorshidnia20-Feb-12 14:09 
Good job
QuestionCongratulations! Pinmemberjoseycaremi14-Dec-11 19:41 
Generalgood job! Pinmemberbatsword2-Sep-11 17:26 
QuestionWhere did you get the black forms you are showing? PinmemberJoe Sonderegger14-Feb-11 4:47 
AnswerRe: Where did you get the black forms you are showing? PinmemberDavid Veeneman14-Feb-11 13:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 31 Dec 2009
Article Copyright 2009 by David Veeneman
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid