« Microsoft Max - The WPF Functional Demo | Main | WPF 3D Not ready? »

WPF Decorators - Build your own "Chrome!"

If you've taken a deep look at how some of the base controls in WPF are constructed, you'll likely have encountered a mysterious element/UIElement at some point, "Chrome." Chrome is a tad confusing when you first encounter it as it seems so magical. Many controls are rendered nearly entirely using these "Chrome" elements (like a Button for example).

One of the first "Chromes" I encountered was actually a Chrome designed for the standard WPF Buttons (ChromeButton). The actual implementation of the "Chrome" for a button may be found in the Microsoft.Windows.Themes namespace. The somewhat interesting part about the Chrome class is that depending on the current user interface style of Windows that is running, a different and unique Chrome assembly is loaded. For example, there's a Chrome library for the Luna (blue XP) theme named PresentationFramework.Luna. There's another for the Vista Aero style named PresentationFramework.Aero.

On my system, the standard WPF button using the Silver XP theme looks like this:

Plain and simple. The button though has some annoying features that make it hard to style any further.

Let's imagine that instead of the standard Silver button, I'd like to make it Green to represent an OK button:

Not too bad so far. The button is green. What about when I actually try to use it ...

Interesting. When I move my mouse over the Button, it reverts back to the original colors. Bummer. What about if I were to click the button ...

Still no green! To say the least, that's a bit annoying. Why Microsoft made the button work this way, I can't answer. I can however provide you with some alternatives.

One solution is to roll your own control template, use the right combination of Borders, rectangles, etc. The sky's the limit. So is, unfortunately, the number of UIElements more than likely. In the first version of WPF that is to be released, UIElements represent an expensive and limited resource in the system, worse in some ways than the old GDI limits, but different in others. Still, it doesn't hurt to conserve, so Microsoft provided another way -- a way to custom render a button like this in a nice reusable pattern. That's exactly what the "Chrome" objects represent in WPF. A fast and lightweight method for rendering a potentially complicated visual. Be warned though that this isn't something that a non-developer would normally do. It's relatively straightforward but certainly outside of the typical non-coder realm. The great thing though is that once developed, it's easy to reference the control either using Expression Interactive Designer or Visual Studio 2005 and use it without understanding how it was built!

My experience has been that it can be EXTREMELY challenging to make some types of "looks" using strictly the UI elements and associated geometries that exist in WPF. Sometimes it seems impossible.

I had a need for a better button. One that would allow me to set different colors AND respect the color in the various typical states (although I have ignored disabled for right now as I had no need. Easy to add though...). The standard button also doesn't provide for a method for changing the border thickness, so I've also provided that functionality.

Here's are the basic steps to creating the Decorator and using it ... (see the attached C# solution with the SmartBorder control and a simple test project that reproduces the screen shot below).

I created a new class library assembly and referenced the PresentationCore/Framework assemblies.

Next, I added a class to the project, deriving from the Decorator base class:

public class SmartBorder : Decorator

To the SmartBorder class, I added a series of DependencyProperty instances for the various necessary properties. For example, here's the CornerRadiusProperty.

public static readonly DependencyProperty CornerRadiusProperty =

DependencyProperty.Register("CornerRadius", typeof(double), typeof(SmartBorder), new FrameworkPropertyMetadata(8.0, FrameworkPropertyMetadataOptions.AffectsRender));

There are two very important steps that must be done to make the new decorator useful: 1) Draw the control, and 2) Measure the control.

The first is accomplished by overriding the OnRender method of the Decorator. Here you're given free access to the DrawingContext (think of this as the place where you draw the series of commands that make up your user interface look). In my decorator, I've used some basic things like the ability to draw a rounded rectangle (see the source code for the other commands I used):

dc.DrawRoundedRectangle(backBrush, borderPen, rc, cornerRadiusCache, cornerRadiusCache);

dc.DrawRoundedRectangle(gradientOverlay, borderPen, rc, cornerRadiusCache, cornerRadiusCache);

Additionally, I wanted to do some drawing which might have occasionally drawn outside of the normal decorator bounds. I didn't want that to happen so I used the clip functionality:

dc.PushClip(new RectangleGeometry(rc, cornerRadiusCache, cornerRadiusCache));

This (and the corresponding dc.Pop) will Clip any drawing commands to the specified shape or rectangle. In this case, I specified a rounded rectangle which mirrors the shape of the button based on the corner radius).

In the override of MeasureOverride, it's the responsibility of the control to respond with the overall desired size of the control. My code simply takes into account the size of the border and adjusts based on the child and returns the overal size. Nothing too complicated.

Additionally, in order to actually use the new SmartBorder, I created a new Control Template:

<ControlTemplate TargetType="{x:Type Button}" x:Key="ShinyButtonTemplate">
<WPD:SmartBorder RenderIsPressed="{TemplateBinding IsPressed}" Background="{TemplateBinding Background}" RenderIsMouseOver="{TemplateBinding IsMouseOver}"
CornerRadius="4"
OuterGlowBrush="{DynamicResource OuterGlowBrush}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
Margin="{TemplateBinding Padding}"
/>
</WPD:SmartBorder>
</ControlTemplate>

The SmartBorder is part of the WPD namespace which was declared in the root element of the host:

xmlns:WPD="clr-namespace:WiredPrairie.Decorators;assembly=WiredPrairie.Decorators"

Since my ControlTemplate specifies that all buttons should use the specified control template, it's easy to see the results (actually, only those buttons that don't override the default by using a new style or control template combination):

<Button Background="Aqua">
<StackPanel HorizontalAlignment="Left">
<Label>Big Label</Label>
<Label Style="{DynamicResource ShinyButtonMiniLabelStyle}">Small Label</Label>
</StackPanel>
</Button>

If you have any questions please e-mail. The source code for the sample project and the smart border control is attached (tested only with RC1 of .NET 3.0). If you happen to use the code somewhere / in something, I'd love to know!

WPSmartBorder.zip (41 KB)

If you use VB.NET, just compile the solution and use the WiredPrairie.Decorators.dll in your VB projects by referencing it.

Enjoy.

Comments

Hey man Sweet code. I modified it a little to make it more flexable. I can send you the file if you want! Email me and let me know what you think.

Help support my web site by searching and buying through Amazon.com (in assocation with Amazon.com).