Introduction
Our designer gave me some cool Photoshop screen drafts of what she thought our application ought to look like. The drafts had several examples of where she had made text with a halo outline around the words. Like all good designers, she had sliced the images up into discrete sections, allowing me to do the integration easily. The problem with this approach was that it was time consuming to integrate each image she gave me, and when we wanted to localize the product later, we would need to redo them all. I needed a way to generate these on the fly.
The Plan
WinForms' System.Drawing.*
provides very powerful ways of creating images with alpha blending. The plan was to create a bitmap for the text. The text would be smeared around in a background color on the bitmap, and then finally written in the middle in the foreground color on the bitmap. Alpha blending would be applied to the background, smeared, version of the text so that it looked feathery towards the edges and could be rendered over any other background. (Alpha valued colors, added on top of alpha valued colors will increase the intensity, so the very edges should look dim, and the closer to the text you are, the more intense the background color will be).
I expected this to take a little while to do, so I planned on having a function return the bitmap for the final text image so that I could squirrel it away and use that as a cached version of the text from that point onwards, thus only suffering the performance hit once.
This seemed like a workable solution, so I set about implementing it...
The Implementation
This was going to take some brushes, some bitmaps, some graphics contexts etc., and we may call this function hundreds of times, so judicious resource management would be important. For those that don't know, when a managed object goes out of scope, it is marked as "unreferenced" to be garbage collected automatically, later in time. When? You may have no idea if you don't explicitly call garbage collection yourself (not necessarily recommended if you use things like weak references, etc., but that is beyond the scope of this article). These managed objects, like the SolidBrush
class, for instance, are, in real life, managed wrappers on GDI+ unmanaged objects, and these objects behind the scenes may consume valuable system resources and memory. Thus, you should not just allow a Brush
or Pen
or Bitmap
or any of the myriad of these wrappers on unmanaged drawing objects simply to fall out of scope in the hope they will be cleaned up soon, as they will insidiously eat precious resources. You need to call Dispose
on each of them to clean up the unmanaged resources. A preferred method is to use the using
construct. "using
" will cause the object's Dispose
method to be called when you leave the using()
scope (which will, in turn, free the underlying GDI+ object). You could call Dispose
yourself at the end of your function, but if you exited the function before calling Dispose
, or if you had an exception before Dispose()
, then you would have the same resource problem you had before. using
ensures that no matter what happens to cause the control to leave the scope of the using()
statement, the object's Dispose
method would be called.
using (SolidBrush brFore=new SolidBrush(clrFore))
{
... use the brush inside of here
}
In order to make the containing bitmap, we will need to know how big the text will be in pixels. We use the Graphics
method MeasureString
to determine this. MeasureString
comes in various flavors, some taking into account alignment and spacing of text etc. We use the basic call as we don't intend on setting any exotic flags when we finally call Graphics.DrawString
. From the returned size, we can make a bitmap that will contain a single rendering of the text which we will use as a basis for rendering onto a second, destination bitmap, several times, to make the blurred background.
SizeF sz=g.MeasureString(strText, fnt);
GDI+ is built for both speed and beauty, and by default, it picks a happy medium. There are properties you can set on a Graphics
object that will ensure you get the best possible rendering. We use SmoothingMode
, InterpolationMode
, and TextRenderingHint
to control the level of output we require.
gBmp.SmoothingMode=SmoothingMode.HighQuality;
gBmp.InterpolationMode=InterpolationMode.HighQualityBilinear;
gBmp.TextRenderingHint=TextRenderingHint.AntiAliasGridFit;
From the bitmap, we create a graphics context (Graphics.FromImage
). This will allow us to draw the string onto the bitmap directly. We make some brushes that we know we will need a little later here, so that we can keep all the using
statements bunched together, and avoid overly nesting this function, to aid readability. Also, we fully expect this function to always complete all the way through, so the marginal overhead over disposing of this object exactly when they are not needed is not worth the expense of readability and maintainability of this function. Note the alpha value of 16 on the brBack
brush, this is a very transparent value, but will be overlaid many times as we smear the background.
using (Bitmap bmp=new Bitmap((int)sz.Width,(int)sz.Height))
using (Graphics gBmp=Graphics.FromImage(bmp))
using (SolidBrush brBack=new
SolidBrush(Color.FromArgb(16,clrBack.R,
clrBack.G, clrBack.B)))
using (SolidBrush brFore=new SolidBrush(clrFore))
{
...
}
We now create another image, made bigger by blurAmount
, to accommodate the smearing, and proceed to get a Graphics
from that so that we can render on the first bitmap we created onto it, and draw it blurAmount
times in the X direction for every blurAmount
times in the Y direction. This rectangular blur approximates a more traditional rounded blur as the alpha values towards the outside are sufficiently low as to make it feather; to be more accurate, you would base the rendering in a circle from the midpoint. The simplification is justified in this case, again for readability, ease of coding, and because the difference would be slight.
bmpOut=new Bitmap(bmp.Width+blurAmount,bmp.Height+blurAmount);
...
for (int x=0;x<=blurAmount;x++)
for (int y=0;y<=blurAmount;y++)
gBmpOut.DrawImageUnscaled(bmp,x,y);
After rendering the blur, we finally render the actual text again, in the center (blurAmount
/2 offset in both X & Y positions) in the foreground color.
gBmpOut.DrawString(strText, fnt, brFore,
blurAmount/2, blurAmount/2);
Using the code
Here is how you would call the code to generate an image for some text, and how you would render the final fancy text image onto a Windows form:
public class Form1 : System.Windows.Forms.Form
{
private Bitmap _bmpText;
public Form1()
{
this.BackColor = System.Drawing.Color.IndianRed;
this.ClientSize = new System.Drawing.Size(358, 126);
using (Font fnt=new Font("Arial", 20, FontStyle.Bold))
_bmpText = (Bitmap) FancyText.ImageFromText("Hello Code" +
" Project Fans!", fnt, Color.Green, Color.Yellow);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawImageUnscaled(_bmpText, 10, 40);
}
}
History