Dynamically Generating Icons (safely)






4.83/5 (38 votes)
May 20, 2004
6 min read

159690

4983
How to dynamically generate an icon in a restricted security zone, as with "No touch deployment" WinForms.
Introduction
Creating images dynamically in .NET is very easy, using the managed GDI+ methods. However, when it comes to creating icons dynamically, things are not always so straightforward. I was working on a Smart Client/"No Touch Deployment" application that would sit in the tray and display some statistics. I figured it would be simple enough: add the NotifyIcon
component to a Form
, set the Form
’s ShowInTaskbar
property to false
, and use the Timer
component to trigger periodic updates to the NotifyIcon
’s Icon
property with an image created from the latest data. Little did I know how difficult it would be to update the icon programmatically. Fortunately, I was able to create a working solution that I will provide in this article.
First thoughts
I realize I could have stored a bunch of different pre-built icons as resources in my executable and swapped them out appropriately. This was not an acceptable solution, as there were too many different possible icon images.
I wanted to be able to generate the images dynamically, using the GDI+ methods exposed by the System.Drawing.Graphics
object. Unfortunately, there is no way to get a Graphics
object for an Icon
. I assumed I could use Graphics.FromImage()
but realized the Icon
class does not derive from Image
. I then tried Graphics.FromHdc()
and Graphics.FromHwnd()
with the Icon.Handle
, and I only got OutOfMemory
Exceptions.
Handles are "unsafe"
So, my next thought was to use the Bitmap
class to create an image, and then copy the image to the Icon
. I noted that the Bitmap
class has a GetHIcon()
method that returns a handle to an Icon
. I could just pair that with the Icon
’s .FromHandle()
factory method to create the icon from the bitmap. And I was right… almost.
Bitmap bmp = new Bitmap(16, 16);
using (Graphics g = Graphics.FromImage(bmp))
g.FillEllipse(Brushes.Red, 0, 0, 16, 16);
notifyIcon1.Icon = Icon.FromHandle(bmp.GetHicon());
This approach worked perfectly, running from the IDE. So I built the executable, copied it to my webserver, and launched it from a web page. Boom! Setting the new icon caused a System.Security.SecurityException
. Apparently, the Icon.FromHandle()
method demands System.Security.Permissions.SecurityPermission
with the UnmanagedCode SecurityPermissionFlag
. Since my goal is to create a “No Touch Deployment” application, I cannot use unmanaged code, which means I cannot use handles. If your application will not be launched from a web page, or you are able to configure the security zones of your clients to allow UnmanagedCode
, you do not need to read any further. The code above will work for your solution.
Not all ImageFormats are created equal
Since I cannot use the Icon.FromHandle()
method, I had to find another way to create an instance of an Icon
. It turns out the Icon
class has a constructor that takes a Stream
parameter. I could save the Bitmap
to a stream, and then load that stream into my Icon
. The Bitmap.Save()
method takes two parameters: the target stream and an ImageFormat
enumeration that specifies how the image will be represented as bits (we want the Icon
format).
// bmp contains the dynamically generated image
MemoryStream buffer = new MemoryStream();
bmp.Save(buffer, ImageFormat.Icon);
// reset the stream back to the beginning so that it can be read
buffer.Position = 0;
notifyIcon1.Icon = new Icon(buffer);
Unfortunately, when you run this code, you get a bit of a confusing error:
System.ArgumentNullException: Value cannot be null.
Parameter name: encoder
at System.Drawing.Image.Save(Stream stream, ImageCodecInfo encoder,
EncoderParameters encoderParams)
at System.Drawing.Image.Save(Stream stream, ImageFormat format)
At this point, I was stumped. I posted to the Windows Forms newsgroups and was directed to a knowledge base article that explains that GDI+ does not include the necessary encoder to save as ImageFormat.Icon
. A Microsoft representative responded saying the only solution was to convert the image into an Icon stream myself. Details of the icon file format could be found on MSDN. So, that is what I did.
The Solution
The Framework supports saving a Bitmap
to a stream using ImageFormat.Bmp
, which saves the bits in the Device Independent Bitmap format. I was able to find the specifications for the DIB format on a game programming site and in the Platform SDK.
For my solution, I created a BitmapHolder
class that holds the contents of the bitmap in structures as they are defined in the documentation. I also created an IconHolder
class that holds the contents of an icon resource using the documented icon structures. Both classes need to be able to load and save their contents using a stream, so I added the following methods:
public void Open(Stream stream){}
public void Save(Stream stream){}
These methods just needed to read or write to the underlying structures that made up the image format. The reading and writing was simplified by adding these methods to each structure:
public void Populate(BinaryReader br){}
public void Save(BinaryWriter bw){}
These methods read/write the structure fields in the order in which they are supposed to appear in the stream. To illustrate the simplicity, I have included the implementations for the BITMAPINFOHEADER
struct
that is used by both the icon and the bitmap format.
public void Populate(BinaryReader br)
{
biSize = br.ReadUInt32();
biWidth = br.ReadInt32();
biHeight = br.ReadInt32();
biPlanes = br.ReadUInt16();
biBitCount = br.ReadUInt16();
biCompression = br.ReadUInt32();
biSizeImage = br.ReadUInt32();
biXPelsPerMeter = br.ReadInt32();
biYPelsPerMeter = br.ReadInt32();
biClrUsed = br.ReadUInt32();
biClrImportant = br.ReadUInt32();
}
public void Save(BinaryWriter bw)
{
bw.Write(biSize);
bw.Write(biWidth);
bw.Write(biHeight);
bw.Write(biPlanes);
bw.Write(biBitCount);
bw.Write(biCompression);
bw.Write(biSizeImage);
bw.Write(biXPelsPerMeter);
bw.Write(biYPelsPerMeter);
bw.Write(biClrUsed);
bw.Write(biClrImportant);
}
The Open
methods on the holder classes simply create a BinaryReader
on the stream, and then pass it to each of their structures’ Populate
methods. The Save
methods on the holder classes do the same with a BinaryWriter
and their structures’ Save
methods.
I tested the BitmapHolder
class by loading bitmap files from disk using Open()
, peeking at the data in memory in debug mode, and then saving them to another filename using Save()
. Once everything was working correctly, the saved file would look exactly like the original file. I repeated this process with the IconHolder
class to confirm it was operating correctly.
At this point, I was confident that my holder classes could read and write to their respective formats. Now, the trick was to use the contents of a BitmapHolder
class to populate an IconHolder
class. I created a Converter
helper class with a static
method to perform the conversion:
public static IconHolder BitmapToIconHolder(BitmapHolder bmp)
This method does all of the real work. I added some overloads to the Converter
class to make it more user-friendly. With the final solution, creating a dynamic icon is as easy as:
Bitmap bmp = new Bitmap(16, 16, PixelFormat.Format24bppRgb);
using (Graphics g = Graphics.FromImage(bmp))
{
g.FillEllipse(Brushes.Red, 0, 0, 16, 16);
g.FillRectangle(Brushes.White, 4, 6, 8, 4);
}
notifyIcon1.Icon = Converter.BitmapToIcon(bmp);
Using the code
All of the necessary code can be found in the demo project, in a subfolder named FlimFlan.IconEncoder. Copy that folder to your own project folder, and include the files in your project. I didn't feel the need to compile it into its own separate assembly, but you can. Create your Bitmap
using the managed GDI+ functions, and then call FlimFlan.IconEncoder.Converter.BitmapToIcon(bitmap)
to return a new Icon
.
Constraints
Note that the implementation of the conversion is not complete for all scenarios. I implemented just enough to solve my specific problem. It currently has the following constraints:
- The source
Bitmap
must usePixelFormat.Format24BppRgb
orPixelFormat.Format32bppArgb
- The source
Bitmap
must use at most 256 colors when usingFormat24BppRgb
- The source
Bitmap
must be 16x16 pixels - The target
Icon
must be 16x16 pixels - The pixel in the lower left corner (0, 15) is used to determine the transparency color
I tried to comment the sections of codes where these assumptions were made, so the conversion could be made more robust. If you add support for more color depths or image sizes, please send me the code, and I will update the article and give you credit.
History
- 20 May 2004 - Initial release.
- 27 Nov 2004 - Updated code: thanks to Pavel Janda for adding support for 32bpp bitmaps.