Sunday, November 05, 2006

From the shadows

When styling controls that have a Popup element (e.g. menus, combo boxes, etc.) a common visual cue is a drop-shadow below the pop-up. WPF contains a DropShadowBitmapEffect that will do this all for us, hence,

<Border ...>
<Border.BitmapEffect>
<DropShadowBitmapEffect Softness=".5" ShadowDepth="5" Color="Black"/>
</Border.BitmapEffect>
</Border>


The result is shown on the left below. However, the DropShadowBitmapEffect is a bit overkill for our needs. It is designed to add drop-shadows to arbitrary shapes (for example the text below) whereas a pop-up is generally rectangular.


If you inspect the default WPF styles you will find that a drop shadow is applied via a Decorator called Microsoft.Windows.Themes.SystemDropShadowChrome. This however is hidden away in PresentationFramework.Luna.dll and designed for use by the default styles.

For my controls I have developed my own decorator to do a similar job. Firstly I have decided to make some assumptions to improve performance - namely that the drop-shadow colour is always black, and that it is always to the bottom-right. All that is required then is to implement a custom "Decorator". Decorators are controls that add some additional rendering to a single element (of course that element may be a Panel that contains several other elements). We can then draw a drop-shadow in the OnRender method consisting of a set of linear and radial gradients to mimic the shadow.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows

class ShadowChrome : Decorator
{
// *** Fields ***

private static SolidColorBrush backgroundBrush;
private static LinearGradientBrush rightBrush;
private static LinearGradientBrush bottomBrush;
private static RadialGradientBrush bottomRightBrush;
private static RadialGradientBrush topRightBrush;
private static RadialGradientBrush bottomLeftBrush;

// *** Constructors ***

static ShadowChrome()
{
MarginProperty.OverrideMetadata(typeof(ShadowChrome), new FrameworkPropertyMetadata(new Thickness(0, 0, 4, 4)));

CreateBrushes();
}

// *** Overriden base methods ***

protected override void OnRender(DrawingContext drawingContext)
{
// Calculate the size of the shadow

double shadowSize = Math.Min(Margin.Right, Margin.Bottom);

// If there is no shadow, or it is bigger than the size of the child, then just return

if (shadowSize <= 0 || this.ActualWidth < shadowSize*2 || this.ActualHeight < shadowSize * 2)
return;

// Draw the background (this may show through rounded corners of the child object)

Rect backgroundRect = new Rect(shadowSize, shadowSize, this.ActualWidth - shadowSize, this.ActualHeight - shadowSize);
drawingContext.DrawRectangle(backgroundBrush, null, backgroundRect);

// Now draw the shadow gradients

Rect topRightRect = new Rect(this.ActualWidth, shadowSize, shadowSize, shadowSize);
drawingContext.DrawRectangle(topRightBrush, null, topRightRect);

Rect rightRect = new Rect(this.ActualWidth, shadowSize * 2, shadowSize, this.ActualHeight - shadowSize * 2);
drawingContext.DrawRectangle(rightBrush, null, rightRect);

Rect bottomRightRect = new Rect(this.ActualWidth, this.ActualHeight, shadowSize, shadowSize);
drawingContext.DrawRectangle(bottomRightBrush, null, bottomRightRect);

Rect bottomRect = new Rect(shadowSize * 2, this.ActualHeight, this.ActualWidth - shadowSize * 2, shadowSize);
drawingContext.DrawRectangle(bottomBrush, null, bottomRect);

Rect bottomLeftRect = new Rect(shadowSize, this.ActualHeight, shadowSize, shadowSize);
drawingContext.DrawRectangle(bottomLeftBrush, null, bottomLeftRect);
}

// *** Private static methods ***

private static void CreateBrushes()
{
// Get the colors for the shadow

Color shadowColor = Color.FromArgb(128, 0, 0, 0);
Color transparentColor = Color.FromArgb(16, 0, 0, 0);

// Create a GradientStopCollection from these

GradientStopCollection gradient = new GradientStopCollection(2);
gradient.Add(new GradientStop(shadowColor, 0.5));
gradient.Add(new GradientStop(transparentColor, 1.0));

// Create the background brush

backgroundBrush = new SolidColorBrush(shadowColor);

// Create the LinearGradientBrushes

rightBrush = new LinearGradientBrush(gradient, new Point(0.0, 0.0), new Point(1.0, 0.0));
bottomBrush = new LinearGradientBrush(gradient, new Point(0.0, 0.0), new Point(0.0, 1.0));

// Create the RadialGradientBrushes

bottomRightBrush = new RadialGradientBrush(gradient);
bottomRightBrush.GradientOrigin = new Point(0.0, 0.0);
bottomRightBrush.Center = new Point(0.0, 0.0);
bottomRightBrush.RadiusX = 1.0;
bottomRightBrush.RadiusY = 1.0;

topRightBrush = new RadialGradientBrush(gradient);
topRightBrush.GradientOrigin = new Point(0.0, 1.0);
topRightBrush.Center = new Point(0.0, 1.0);
topRightBrush.RadiusX = 1.0;
topRightBrush.RadiusY = 1.0;

bottomLeftBrush = new RadialGradientBrush(gradient);
bottomLeftBrush.GradientOrigin = new Point(1.0, 0.0);
bottomLeftBrush.Center = new Point(1.0, 0.0);
bottomLeftBrush.RadiusX = 1.0;
bottomLeftBrush.RadiusY = 1.0;
}
}



This may then be used as shown in the following XAML. Note that the Margin property is used for two uses. Firstly it provides an area around the element to render the drop-shadow (this is important in a Popup since it ensures the drop-shadow is within the pop-up window), and also specifies to ShadowChrome the size to render the drop-shadow.

<ShadowChrome>
<Border Margin="0,0,5,5" .../>
</ShadowChrome>

10 comments:

Livingston said...

Andy,

Thank you so much. This is exactly what I needed.

I'm now using your decorator in a project available at http://designer.cooltext.com

I did make a few changes to it though.

Hard coded size, since I didn't want to mess with a margin.

Made the shadow softer. Start at 64 alpha and fade to zero.

Moved the topRight gradient up a bit and the bottomLeft gradient left a bit.

Bryan

Livingston said...

One other thing, the pipes (||) in the or clauses in OnRender didn't seem to get posted.

Unknown said...

Bryan,

Pleased you found a use for this code. I've not done any real perf testing but I very much suspect it is going to be faster than a shadow bitmap effect.

As for hardcoding the size, I know the margin setting the shadow size is a little hacky. My main argument for this is that within a Popup you can't draw outside the popup area, hence you need to put a margin on the content to give room for the shadow effect. Since the margin is required anyway, I thought I'd reuse this for the shadow.

PS. I've updated the post with the pipes. Thanks for that.

Anonymous said...

I want to know two things

1, if you can create some code to implement the feature in .NET 2.0

2, how to compare this with dropshadown in PS. If I can creat an drop shadow in PS with angle,spread,distance etc. how to create the same image with DropShadowBitmapEffect?

Unknown said...

Anonymous,

1. Implementing the same effect in .Net 2.0 (i.e. WinForms) would be possible, but in this case the rendering would have to be by a separate custom control, using GDI+ for the drawing. Either that or use some form of owner-draw. I think it just goes to show how much easier these things are in WPF - just add the drop-shadow into a template.

2. The code I show in my blog post is designed to be a simple, but better performing shadow. If you wish to specify an angular shadow direction then DropShadowBitmapEffect would be the way to go. This allows you to set colour, direction, shadow depth, etc. as required.

homer said...

I'm a WPF newbie.

Just found this post. It looks great. I am having some serious performance issues with 2 large dropshadows right now. Seems that the biger the shadow, the worse things get.

I would love to use this code. I have it in a class in my project, I just have no idea how to get a reference to it in my xaml so I can use the tags.

ShadowChrome
Border Margin="0,0,5,5" ...
/ShadowChrome

Can you please make a quick post with a more detailed explanation of how to implement the class ?

Thanks a bunch

Unknown said...

homer,

To use the ShadowChrome in your code there are a few steps you need to perform.


(1) Include the ShadowChrome class into your app. Feel free to simply copy the code into your current solution.


(2) Put ShadowChrome into a suitable namespace (this will change to be consistent with the namespaces of the rest of your app). For example,

namespace MyApp.Decorators
{
class ShadowChrome : Decorator
{
...
}
}


(3) Now you need some way of referencing this namespace in XAML. To do this add in your root element something like,

xmlns:dec="clr-namespace:MyApp.Decorators"


(4) Now anywhere you need to use ShadowChrome simply refer to it as,

<dec:ShadowChrome>
...
</dec:ShadowChrome>


For more technical information check out,

http://msdn2.microsoft.com/en-us/library/ms747086.aspx

or search for 'clr-namespace' in google for more examples.

homer said...

Yep. That got it. Thanks. It works great. I did modify it a bit to darken the shadow and soften the gradient a bit though.

Just for reference sake, I had a window with 2 large dropshadows and a bunch of user controls that get added dynamically. With the WPF drop shadows enabled the response time on Clicking a button was pushing 3 seconds - unacceptable. After implementing your code response is almost instant. Obviously A HUGE performance gain.

Thanks again.

Unknown said...

homer,

Pleased that helps. The performance improvement you are seeing is impressive.

Note that my code only supports rectangular regions for the drop-shadow. One reason the WPF version is so slow is that it can handle any shapes correctly. The main reason for the slow down though is that it forces software rendering. This looks to all change with the next version of WPF (summer 2008-ish?) which hardware enables bitmap effects. This will significantly improve rendering times for drop-shadows.

Anonymous said...

That sure is useful - thanks!