Sichbo 的答案仍然很好,但仅适用于方形矩形。
我已经对其进行了调整以添加 CornerRadius 属性,以便能够在圆角矩形上绘制阴影,以便它与边框控件很好地配合。
我还将渐变计算更改为类似于我计算机上实际的 DropShadowEffect 的东西,但由于它具有非常特殊的渲染,因此似乎无法完美再现。在任何情况下,您都可以尝试使用expFactor 和attenuationFactor 来适应您的需求。 expFactor 控制阴影消失的速度,attenuationFactor 通过偏移范围使其更轻(而不是从 100% 到 0% 阴影,我们从例如 95% 到 -5%,上限为 0 %)。
渐变是在设置相关属性时计算的,而不是在每次渲染调用时计算的。
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;
}
}
}