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.
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...
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
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
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
TextRenderingHint to control the level of output we require.
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
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.
for (int x=0;x<=blurAmount;x++)
for (int y=0;y<=blurAmount;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,
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;
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);