The answer by Sichbo is still excellent, but only works with square rectangles.
I have tweaked it to add the CornerRadius property, in order to be able to draw shadows on round rectangles so that it works well with Border controls.
I also changed the gradient calculation to something that resembles the actual DropShadowEffect on my computer, but since that one has a very peculiar rendering it seems impossible to reproduce perfectly. In any case, you can try to play with the expFactor
and attenuationFactor
to adapt it to your needs. expFactor
controls the speed at which the drop shadow vanishes, and attenuationFactor
makes it lighter by offsetting the range (instead of going from 100% to 0% shadow, we go from e.g. 95% to -5%, capped at 0%).
The gradients are calculated when the related properties are set, instead of at each render call.
namespace CustomControls
{
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
/// <summary>
/// Emulates the System.Windows.Media.Effects.DropShadowEffect using rectangles and
/// gradients, which performs a lot better.
/// </summary>
public class FastShadow : Decorator
{
public static readonly DependencyProperty ColorProperty =
DependencyProperty.Register("Color", typeof(Color), typeof(FastShadow),
new FrameworkPropertyMetadata(
Color.FromArgb(0x71, 0x00, 0x00, 0x00),
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback((o, e) =>
{
FastShadow f = o as FastShadow;
f.CalculateGradientStops();
})));
public static readonly DependencyProperty BlurRadiusProperty =
DependencyProperty.Register("BlurRadius", typeof(double), typeof(FastShadow),
new FrameworkPropertyMetadata(
10.0,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback((o, e) =>
{
FastShadow f = o as FastShadow;
if ((double)e.NewValue < 0)
f.BlurRadius = 0;
f.CalculateGradientStops();
})));
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof(double), typeof(FastShadow),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback((o, e) =>
{
FastShadow f = o as FastShadow;
if ((double)e.NewValue < 0)
f.BlurRadius = 0;
f.CalculateGradientStops();
})));
public static readonly DependencyProperty ShadowDepthProperty =
DependencyProperty.Register("ShadowDepth", typeof(double), typeof(FastShadow),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback((o, e) => {
FastShadow f = o as FastShadow;
if ((double)e.NewValue < 0)
f.ShadowDepth = 0;
})));
public static readonly DependencyProperty DirectionProperty =
DependencyProperty.Register("Direction", typeof(int), typeof(FastShadow),
new FrameworkPropertyMetadata(315, FrameworkPropertyMetadataOptions.AffectsRender));
private GradientStopCollection gradientStops;
private Brush[] edgeBrushes;
private Brush[] cornerBrushes;
public FastShadow()
: base()
{
this.CalculateGradientStops();
}
/// <summary>
/// Color used to fill the shadow region.
/// </summary>
[Category("Common Properties")]
public Color Color
{
get => (Color)this.GetValue(ColorProperty);
set
{
this.SetValue(ColorProperty, value);
}
}
/// <summary>
/// Size of the shadow.
/// </summary>
[Category("Common Properties")]
[Description("Size of the drop shadow")]
public double BlurRadius
{
get => (double)this.GetValue(BlurRadiusProperty);
set
{
this.SetValue(BlurRadiusProperty, value);
}
}
/// <summary>
/// Radius of the corners.
/// </summary>
[Category("Common Properties")]
[Description("Radius of the corners of the shadow")]
public double CornerRadius
{
get => (double)this.GetValue(CornerRadiusProperty);
set
{
this.SetValue(CornerRadiusProperty, value);
}
}
/// <summary>
/// Distance from centre.
/// </summary>
[Category("Common Properties")]
[Description("Distance from centre")]
public double ShadowDepth
{
get { return (double)this.GetValue(ShadowDepthProperty); }
set { this.SetValue(ShadowDepthProperty, value); }
}
/// <summary>
/// Angle of the shadow
/// </summary>
[Category("Common Properties")]
[Description("Angle of the shadow")]
public int Direction
{
get { return (int)this.GetValue(DirectionProperty); }
set { this.SetValue(DirectionProperty, value); }
}
/// <summary>
/// Calculate gradient stops for an exponential gradient.
/// It is designed to look similar to the WPF DropShadowEffect, but since that one renders
/// differently depending on how zoomed in you are, a perfect fit seems impossible.
/// </summary>
protected void CalculateGradientStops()
{
double blurRadius = Math.Max(this.BlurRadius, 0);
double cornerRadius = Math.Max(this.CornerRadius, 0);
// Portion of the gradient which is drawn "inside" the border
double innerPart = (cornerRadius - blurRadius / 20) / (cornerRadius + blurRadius);
double remaining = 1.0 - innerPart;
double expFactor = 1.5 + blurRadius / 50;
double attenuationFactor = Math.Max(0.3 - blurRadius / 50, 0.05);
GradientStopCollection gsc = new GradientStopCollection();
float[] stops = new float[] { 0.0f, 0.025f, 0.05f, 0.075f, 0.1f, 0.125f,
0.015f, 0.2f, 0.25f, 0.3f, 0.35f, 0.4f, 0.45f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f };
Color stopColor = this.Color;
gsc.Add(new GradientStop(stopColor, 0.0));
gsc.Add(new GradientStop(stopColor, innerPart));
foreach (float stop in stops)
{
// Exponential gradient from 1 to 0
double num = (Math.Exp(1 - expFactor * (stop + attenuationFactor)) - Math.Exp(1 - expFactor));
double det = (Math.E - Math.Exp(1 - expFactor));
double factor = Math.Max(0, num / det);
stopColor.A = (byte)(factor * this.Color.A);
gsc.Add(new GradientStop(stopColor, innerPart + stop * remaining));
}
stopColor.A = 0;
gsc.Add(new GradientStop(stopColor, innerPart + 1.0 * remaining));
gsc.Freeze();
this.gradientStops = gsc;
this.edgeBrushes = new Brush[]
{
new LinearGradientBrush(this.gradientStops, 0)
{ StartPoint = new Point(0, 1), EndPoint = new Point(0, 0) }, // T
new LinearGradientBrush(this.gradientStops, 0)
{ StartPoint = new Point(0, 0), EndPoint = new Point(1, 0) }, // R
new LinearGradientBrush(this.gradientStops, 0)
{ StartPoint = new Point(0, 0), EndPoint = new Point(0, 1) }, // B
new LinearGradientBrush(this.gradientStops, 0)
{ StartPoint = new Point(1, 0), EndPoint = new Point(0, 0) }, // L
new SolidColorBrush(this.Color),
};
for (int i = 0; i < this.edgeBrushes.Length; i++)
this.edgeBrushes[i].Freeze();
this.cornerBrushes = new Brush[]
{
new RadialGradientBrush(this.gradientStops) { Center = new Point(1, 1),
GradientOrigin = new Point(1, 1), RadiusX = 1, RadiusY = 1 }, // TL
new RadialGradientBrush(this.gradientStops) { Center = new Point(0, 1),
GradientOrigin = new Point(0, 1), RadiusX = 1, RadiusY = 1 }, // TR
new RadialGradientBrush(this.gradientStops) { Center = new Point(0, 0),
GradientOrigin = new Point(0, 0), RadiusX = 1, RadiusY = 1 }, // BR
new RadialGradientBrush(this.gradientStops) { Center = new Point(1, 0),
GradientOrigin = new Point(1, 0), RadiusX = 1, RadiusY = 1 }, // BL
};
for (int i = 0; i < this.cornerBrushes.Length; i++)
this.cornerBrushes[i].Freeze();
}
protected override void OnRender(DrawingContext drawingContext)
{
double distance = Math.Max(this.ShadowDepth, 0);
double blurRadius = Math.Max(this.BlurRadius, 0);
double cornerRadius = Math.Max(this.CornerRadius, 0);
double totalRadius = blurRadius + cornerRadius;
double angle = this.Direction + 45; // Make it behave the same as DropShadowEffect
Rect shadowBounds = new Rect(new Point(0, 0),
new Size(this.RenderSize.Width, this.RenderSize.Height));
shadowBounds.Inflate(blurRadius, blurRadius);
double angleRad = angle * Math.PI / 180.0;
double xDispl = distance;
double yDispl = distance;
double newX = xDispl * Math.Cos(angleRad) - yDispl * Math.Sin(angleRad);
double newY = yDispl * Math.Cos(angleRad) + xDispl * Math.Sin(angleRad);
TranslateTransform translate = new TranslateTransform(newX, newY);
Rect shadowRenderRect = translate.TransformBounds(shadowBounds);
Color color = this.Color;
// Build a set of rectangles for the shadow box
Rect[] edges = new Rect[]
{
new Rect(new Point(shadowRenderRect.X + totalRadius, shadowRenderRect.Y),
new Size(Math.Max(shadowRenderRect.Width - (totalRadius * 2), 0), totalRadius)), // T
new Rect(new Point(shadowRenderRect.Right - totalRadius, shadowRenderRect.Y + totalRadius),
new Size(totalRadius, Math.Max(shadowRenderRect.Height - (totalRadius * 2), 0))), // R
new Rect(new Point(shadowRenderRect.X + totalRadius, shadowRenderRect.Bottom - totalRadius),
new Size(Math.Max(shadowRenderRect.Width - (totalRadius * 2), 0), totalRadius)), // B
new Rect(new Point(shadowRenderRect.X, shadowRenderRect.Y + totalRadius),
new Size(totalRadius, Math.Max(shadowRenderRect.Height - (totalRadius * 2), 0))), // L
new Rect(new Point(shadowRenderRect.X + totalRadius, shadowRenderRect.Y + totalRadius),
new Size(
Math.Max(shadowRenderRect.Width - (totalRadius * 2), 0),
Math.Max(shadowRenderRect.Height - (totalRadius * 2), 0))), // C
};
Rect[] corners = new Rect[]
{
new Rect(shadowRenderRect.X, shadowRenderRect.Y, totalRadius * 2, totalRadius * 2), // TL
new Rect(shadowRenderRect.Right - totalRadius * 2, shadowRenderRect.Y,
totalRadius * 2, totalRadius * 2), // TR
new Rect(shadowRenderRect.Right - totalRadius * 2, shadowRenderRect.Bottom - totalRadius * 2,
totalRadius * 2, totalRadius * 2), // BR
new Rect(shadowRenderRect.X, shadowRenderRect.Bottom - totalRadius * 2,
totalRadius * 2, totalRadius * 2), // BL
};
double[] guidelineSetX = new double[] {
shadowRenderRect.X,
shadowRenderRect.X + totalRadius,
shadowRenderRect.Right - totalRadius,
shadowRenderRect.Right, };
double[] guidelineSetY = new double[] {
shadowRenderRect.Y,
shadowRenderRect.Y + totalRadius,
shadowRenderRect.Bottom - totalRadius,
shadowRenderRect.Bottom, };
drawingContext.PushGuidelineSet(new GuidelineSet(guidelineSetX, guidelineSetY));
for (int i = 0; i < edges.Length; i++)
drawingContext.DrawRectangle(this.edgeBrushes[i], null, edges[i]);
drawingContext.DrawGeometry(this.cornerBrushes[0], null, CreateArcDrawing(corners[0], 180, 90));
drawingContext.DrawGeometry(this.cornerBrushes[1], null, CreateArcDrawing(corners[1], 270, 90));
drawingContext.DrawGeometry(this.cornerBrushes[2], null, CreateArcDrawing(corners[2], 0, 90));
drawingContext.DrawGeometry(this.cornerBrushes[3], null, CreateArcDrawing(corners[3], 90, 90));
drawingContext.Pop();
}
/// <summary>
/// Create an Arc geometry drawing of an ellipse or circle.
/// </summary>
/// <param name="rect">Box to hold the whole ellipse described by the arc</param>
/// <param name="startDegrees">Start angle of the arc degrees within the ellipse. 0 degrees is a line to the right.</param>
/// <param name="sweepDegrees">Sweep angle, -ve = Counterclockwise, +ve = Clockwise</param>
/// <returns>GeometryDrawing object</returns>
private static PathGeometry CreateArcDrawing(Rect rect, double startDegrees, double sweepDegrees)
{
// degrees to radians conversion
double startRadians = startDegrees * Math.PI / 180.0;
double sweepRadians = sweepDegrees * Math.PI / 180.0;
// x and y radius
double dx = rect.Width / 2;
double dy = rect.Height / 2;
// determine the center point
double xc = rect.X + dx;
double yc = rect.Y + dy;
// determine the start point
double xs = rect.X + dx + (Math.Cos(startRadians) * dx);
double ys = rect.Y + dy + (Math.Sin(startRadians) * dy);
// determine the end point
double xe = rect.X + dx + (Math.Cos(startRadians + sweepRadians) * dx);
double ye = rect.Y + dy + (Math.Sin(startRadians + sweepRadians) * dy);
bool isLargeArc = Math.Abs(sweepDegrees) > 180;
SweepDirection sweepDirection = sweepDegrees < 0 ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
PathGeometry pathGeometry = new PathGeometry();
PathFigure pathFigure = new PathFigure();
pathFigure.StartPoint = new Point(xc, yc);
LineSegment line = new LineSegment(new Point(xs, ys), true);
pathFigure.Segments.Add(line);
pathFigure.StartPoint = new Point(xs, ys);
ArcSegment arc = new ArcSegment(new Point(xe, ye), new Size(dx, dy), 0, isLargeArc, sweepDirection, true);
pathFigure.Segments.Add(arc);
line = new LineSegment(new Point(xc, yc), true);
pathFigure.Segments.Add(line);
pathFigure.IsFilled = true;
pathGeometry.Figures.Add(pathFigure);
return pathGeometry;
}
}
}