One great new feature in WinFX / Windows Presentation Foundation (Avalon) is the ability to render true drop shadows using the DropShadowBitmapEffect class. What do I mean by true shadows? It's relatively straightforward to simply render text twice onto a form, but in the real world the edges of shadows do not have hard edges. In the real world, light does not come from a single one dimensional point in three dimensional space. The consequence is that shadows have both an umbra and a penumbra - a darker center, surrounded by a gradient from dark to light at the edges. You get this out of the box with Windows Presentation Foundation. However, even though Windows Presentation Foundation now has a Go Live license, not everyone can take a dependency on a beta framework. What I wanted to do was create such an effect using Windows Forms.

Given that we can understand the basic premise (darker middle, surrounded by a gradient from dark to light), we could conceivably render this manually. We could conceivably work with GraphicsPath objects to vectorize fonts, create two paths (one representing the outer edge of the penumbra and the other representing the inner edge of the penumbra / outer edge of the umbra, use a PathGradientBrush for the penumbra, and use a SolidBrush for the umbra. But that sure sounds like a lot of work, both for me and for the graphics subsystem of my trusty Tablet PC. (Using a PathGradientBrush is a fairly expensive operation.)

So my next thought, of course, is ... how can I cheat?

If you were following my previous article on rasterizing images, we looked at several algorithmic options for scaling raster images. If you played around with any of these algorithms, you will begin to notice some of the blurring effects when scaling up images, and in particular if you use the HighQualityBicubic interpolation mode you end up with a nice fuzzy blur.

A nice fuzzy blur.

Around a solid colored center.

Hmm...

That sounds a little bit like what we actually want. Perhaps what is normally a bad thing when scaling up images can be put to productive use in this scenario.

One option for creating more realistic shadows using Windows Forms is to scale down the text that you want when you rasterize it, and then scale it back up using the HighQualityBicubic interpolation mode. Let's take a look at how that looks.

More Realistic Text Shadows in Windows Forms

To achieve this, I derived from the Label class. I added a few properties to describe the shadow, and then I overrode the OnPaint method. When I paint, I create a bitmap that is smaller (scaled based on the Softness property), and then apply a matrix transformation to the graphics object for this bitmap. The matrix transformation includes both scaling (so the font appears at the same relative size when scaled back up) and offset based on the Direction and ShadowDepth properties. After I have rendered onto the bitmap, I render the bitmap onto the label itself, and finally render the text directly onto the Label. The result? A more realistic shadow.

Is this a perfect approach? Far from it. If you manipulate the properties for a while, you will notice that there are sometimes challenges in getting the text to appear at exactly the same size. We expect this. Why? We are rasterizing the same vector two different times. The rasterizer has to determine which pixels are the best fit for the vector at two completely different scales, and you will end up with different rounding errors at each.

Another shortcoming is that Windows Forms will not allow the Label object to render pixels outside of its bounds. As you ratchet up the ShadowDepth property, the bitmap you render will drift further and further off the edge of the Label object's bounds, initially being cut off, and eventually disappearing entirely.

Nonetheless, this is an interesting cheat for obtaining a more realistic effect, and something that is kind of fun to play around with.

Here is the code for my MoreRealisticShadowLabel class:

namespace SoftShadowLabel {

  using System;
  using System.ComponentModel;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Drawing.Imaging;
  using System.Drawing.Text;
  using System.Windows.Forms;


  class MoreRealisticShadowLabel : Label {

    private Color color;
    private int direction;
    private float softness;
    private int opacity;
    private int shadowDepth;

    public MoreRealisticShadowLabel()
      : base() {
      color = Color.Black;
      direction = 315;
      softness = 2f;
      opacity = 100;
      shadowDepth = 4;
    }

    [Category("Appearance")]
    [Description("Gets or sets the color of the shadow")]
    [DefaultValue(typeof(Color), "0x000000")]
    public Color Color {
      get {
        return color;
      }
      set {
        color = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets the degree of opacity of the shadow")]
    [DefaultValue(100)]
    public int Opacity {
      get {
        return opacity;
      }
      set {
        if (value < 0 || value > 255) {
          throw new ArgumentOutOfRangeException("Opacity",
            "Opacity must be between 0 and 255");
        }
        opacity = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets how soft the shadow is")]
    [DefaultValue(2f)]
    public float Softness {
      get {
        return softness;
      }
      set {
        if (softness <= 0) {
          throw new ArgumentOutOfRangeException("Softness",
            "Softness must be greater than 0");
        }
        softness = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets the angle the shadow is cast")]
    [DefaultValue(315)]
    public int Direction {
      get {
        return direction;
      }
      set {
        if (value < 0 || value > 360) {
          throw new ArgumentOutOfRangeException("Direction", 
            "Direction must be between 0 and 360");
        }
        direction = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets the distance between the plane " +
      "of the object casting the shadow and the shadow plane")]
    [DefaultValue(4)]
    public int ShadowDepth {
      get {
        return shadowDepth;
      }
      set {
        if (value < 0) {
          throw new ArgumentOutOfRangeException("ShadowDepth",
            "ShadowDepth must be greater than 0");
        }
        shadowDepth = value;
        Invalidate();
      }
    }

    protected override void OnPaint(PaintEventArgs e) {

      Graphics screenGraphics = e.Graphics;
      Bitmap shadowBitmap = new Bitmap(Math.Max((int)(Width / softness), 1),
        Math.Max((int)(Height / softness), 1));
      using (Graphics imageGraphics = Graphics.FromImage(shadowBitmap)) {
        imageGraphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
        Matrix transformMatrix = new Matrix();
        transformMatrix.Scale(1 / softness, 1 / softness);
        transformMatrix.Translate((float)(shadowDepth * Math.Cos(direction)),
          (float)(shadowDepth * Math.Sin(direction)));
        imageGraphics.Transform = transformMatrix;
        imageGraphics.DrawString(Text, Font,
          new SolidBrush(Color.FromArgb(opacity, color)), 0, 0,
          StringFormat.GenericTypographic);
      }
      screenGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
      screenGraphics.DrawImage(shadowBitmap, ClientRectangle, 0, 0, 
        shadowBitmap.Width, shadowBitmap.Height, GraphicsUnit.Pixel);
      screenGraphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
      screenGraphics.DrawString(Text, Font, new SolidBrush(ForeColor), 0, 0, 
        StringFormat.GenericTypographic);
  
    }

  }
}

You may have noticed that I am using ClearType for the text rendering, but AntiAliasGridFit for the shadow rendering. If I turn on ClearType when rendering the shadowed text to the bitmap, then things really start to go haywire, and I lose transparency altogether.

The same principals can be applied to any vector drawing - this technique is not specific to text. You could also apply this technique to a raster image that includes transparency, although you would have to touch each of the pixels to change the color to your shadow color. With both, you also have to determine if you want to have your shadow alpha vary with your object alpha. Here, we assumed a constant alpha value for our text vectors, although this may not necessarily be a safe assumption.

Let's see if I can talk about something other than rendering text next time around...