Title: Make multi-image icon files in C#
The most interesting thing about icon files is that they can contain several images with different sizes. When a program needs to display the icon, it can pick the size that is appropriate. For example, Windows 10 displays icons in various sizes including 16 x 16, 32 x 32, 48 x 48, and 256 x 256 pixels. It can even scale some icons to produce other sizes. And in "Classic Mode" Windows displays icons with sizes 16 x 16, 24 x 24, 32 x 32, 48 x 48 and 64 x 64 pixels.
The following section explains a basic approach for building multi-image icon files. The section after that describes some changes that I made to the basic approach.
IconFactory
A C# program can save an Icon object into an icon file, but it does not have a simple way to create multi-image icon files.
Fortunately an unnamed poster on Stack Overflow posted an IconFactory class that lets you create multi-image icons. The post is at How to create an Icon file that contains Multiple Sizes / Images in C#.
The basic approach is straightforward, just a lot of work. What this user did was study the specifications for icon files and then write binary data in that format.
Here's the core of the Stack Overflow IconFactory class.
// Write an icon into a stream.
// Note that this closes the stream.
public static void SavePngsAsIcon(
IEnumerable<Bitmap> images,
Stream stream, bool notify_system)
{
Bitmap[] ordered_images =
images.OrderBy(i => i.Width).ThenBy(i => i.Height).ToArray();
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(HeaderReserved);
writer.Write(HeaderIconType);
writer.Write((ushort)ordered_images.Length);
Dictionary<uint, byte[]> buffers =
new Dictionary<uint, byte[]>();
uint length_sum = 0;
uint base_offset =
(uint)(HeaderLength * EntryLength * ordered_images.Length);
for (int i = 0; i < ordered_images.Length; i++)
{
Bitmap image = ordered_images[i];
byte[] buffer = CreateImageBuffer(image);
uint offset = base_offset + length_sum;
writer.Write(GetIconWidth(image));
writer.Write(GetIconHeight(image));
writer.Write(PngColorsInPalette);
writer.Write(EntryReserved);
writer.Write(PngColorPlanes);
writer.Write((ushort)Image.GetPixelFormatSize(
image.PixelFormat));
writer.Write((uint)buffer.Length);
writer.Write(offset);
length_sum += (uint)buffer.Length;
buffers.Add(offset, buffer);
}
foreach (KeyValuePair<uint, byte[]> kvp in buffers)
{
writer.BaseStream.Seek(kvp.Key, SeekOrigin.Begin);
writer.Write(kvp.Value);
}
}
if (notify_system)
{
SHChangeNotify(
HChangeNotifyEventID.SHCNE_ASSOCCHANGED,
HChangeNotifyFlags.SHCNF_IDLIST,
IntPtr.Zero, IntPtr.Zero);
}
}
This method takes as a parameter an IEnumerable of bitmaps. It uses LINQ to sort the images by their sizes, first by width and then by height.
The code then creates a BinaryWriter to write results into the stream passed into the method. It writes some header information into the writer and then loops through the bitmaps. The method writes each bitmap's width, height, and other data into the stream.
One of the more interesting pieces of information that the program writes into the stream is the bitmap's image buffer. The code gets that from the CreateImageBuffer method, which I'll describe shortly. That buffer contains the pixel information that defines the bitmap.
Note that the BinaryWriter closes the original stream when its using block ends. You'll see why this matters later.
After the method has finished writing the icon data into the stream, it checks its notify_system parameter and calls SHChangeNotify if it is true. I'll say more about that in the next section.
The following code shows the CreateImageBuffer method.
private static byte[] CreateImageBuffer(Bitmap image)
{
using (MemoryStream stream = new MemoryStream())
{
image.Save(stream, ImageFormat.Png);
return stream.ToArray();
}
}
This method creates a MemoryStream object to hold data in memory. It then calls the bitmap's Save method to make it save itself into the stream. The method returns the stream converted into an array. That becomes the buffer that the SavePngsAsIcon method uses to represent the bitmap's pixel data.
Changes
That's the basic approach, but I made a few changes to the original version posted on Stack Overflow.
First, I reformatted some of the code to make it easier to read. For example, the original version qualified method calls with the IconFactory class name even though they weren't really necessary. For example, the code used IconFactory.HeaderReserved even though only HeaderReserved was necessary.
I also removed the error handling code that was in the original post. Not that error handling is unimportant, but I wanted to make the example easier to understand. You should definitely include error handling in any program.
Some of the original error handling also seemed a bit excessive. For example, if you try to pass a null array of bitmaps into the original version, the code throws an appropriate exception. If you do that with this version of the class, you'll get a "Value cannot be null" exception. It may be a little harder to track down exactly which value is null, but as I said I wanted to keep the example simpler.
The original version of the CreateImageBuffer method called the bitmap's Save method to save the image in its raw data format. (See the Stack Overflow post to see that.) That only worked with bitmaps loaded from files not created at runtime, so I changed it to ask the bitmap to save itself in PNG format.
My example program saves icon files. Windows caches the images that it displays for icon files, so if you overwrite an existing file with a file that contains new images, Windows does not change the images that you see in places like the desktop and File Explorer. That made testing the program more confusing.
To handle this, I added the notify_system parameter and the call to the SHChangeNotify API function. The new version of the SavePngsAsIcon method uses that function to tell Windows that an icon has changed so it should reload its cache. That makes it display the images that it should for the modified icon file.
If your program is using the SavePngsAsIcon method to create multi-image icons that it uses at runtime, then you should set the notify_system parameter to false so Windows doesn't need to rebuild its cache. If you save icons in icon files, then set the parameter to true so Windows can update appropriately.
The last major change I made to the IconFactory class is to add two new overloaded versions of the SavePngsAsIcon method. The following code shows the first.
// Save an icon into a file.
public static void SavePngsAsIcon(
IEnumerable<Bitmap> images,
string filename, bool notify_system)
{
using (FileStream stream = new FileStream(filename, FileMode.Create))
{
SavePngsAsIcon(images, stream, notify_system);
}
}
This version takes as a parameter a file name. It creates a FileStream to create that file and then uses the previous version of SavePngsAsIcon to write the icon into the FileStream. That saves the icon into the indicated file.
The following code shows the second overloaded version of the SavePngsAsIcon method, which creates an icon an returns it as an Icon object.
// Return an icon as an Icon object.
public static Icon SavePngsAsIcon(
IEnumerable<Bitmap> images)
{
using (MemoryStream stream = new MemoryStream())
{
// Write the icon into the stream.
SavePngsAsIcon(images, stream, false);
// Create a new stream from the first one.
using (MemoryStream stream2 =
new MemoryStream(stream.ToArray()))
{
// Create and return an icon from the stream.
return new Icon(stream2);
}
}
}
This code creates a MemoryStream and calls the first version of SavePngsAsIcon to write the icon into the stream. As I mentioned earlier, SavePngsAsIcon closes the stream, so we can't just rewind it and use it as a stream. Instead this code creates a new stream, initializing it from the first one. It then uses that stream to create a new Icon and returns that Icon.
The Main Program
When the example program starts, it executes the following Load event handler.
private void Form1_Load(object sender, EventArgs e)
{
Bitmap bm16 = MakeBitmap16();
Bitmap bm24 = MakeBitmap24();
Bitmap bm32 = MakeBitmap32();
Bitmap bm48 = MakeBitmap48();
Bitmap bm256 = MakeBitmap256();
pic16.Image = bm16;
pic24.Image = bm24;
pic32.Image = bm32;
pic48.Image = bm48;
pic256.Image = bm256;
Bitmap[] bitmaps = { bm16, bm32, bm24, bm48, bm256 };
// Save the icon into a file.
string filename = "result.ico";
IconFactory.SavePngsAsIcon(bitmaps, filename, true);
// Make this form use the icon.
this.Icon = IconFactory.SavePngsAsIcon(bitmaps);
}
This code calls various MakeBitmapXx methods to create bitmaps in various sizes. Those methods are relatively straightforward so I won't show them here. Download the example to see how they work.
The code then displays those bitmaps in a group of PictureBox controls. If you look closely at the picture at the top of this post, you'll see that the different images are all arranged differently. That's common for icons in various sizes. What looks good at one size would look bad at another. For example, if you simply shrank the 256 x 256 pixel icon to the 16 x 16 pixel size, you wouldn't be able to read any of the text.
After it displays the bitmaps, the code makes an array containing them and passes them into IconFactory.SavePngsAsIcon. It passes that method a file name so the method creates the icon and saves it into the file result.ico. You can find the file in the bin/Debug subdirectory. File Explorer displays the file using an appropriate image depending on the View setting. (Details, Large icons, Extra large icons, etc.) If you drag the file around, Windows will display a 96 x 96 pixel version that it creates by scaling the 256 x 256 pixel image.
Finally, the event handler calls another overloaded version of SavePngsAsIcon to create an Icon object and makes the form use that icon. You can see the small verison of the icon in the picture at the top of this post.
If you use Alt+Tab or Win+Tab, you'll see the 24 x 24 pixel version of the icon above and to the left of the program's image as shown in the picture on the right.
Conclusion
C# does not provide a way to make multi-image icon files, but the IconFactory class does. Its overloaded versions of the SavePngsAsIcon method let you create an icon from a set of bitmaps and save the result into a stream, file, or Icon object.
(Many thanks to the unnamed Stack Overflow user who did the research necessary to create the original version of the IconFactory class.)
Download the example to experiment with it and to see additional details.
|