Reading and Saving .ICO files and resources in VB

This code demonstrates how to read an icon and all the constituent image types, not just the ones the standard APIs or VB allow you to.

Icon Graphic

VB allows you to load an save .ICO files through the LoadPicture and SavePicture methods, which are implemented in the StdPicture object. Unfortunately, this object was really written for 16 bit Windows and doesn't understand Win32 icons at all. A .ICO resource can contain many different images, at different colour depths.

Internet Explorer 5 Icon

Windows uses the different sizes and colours as required in the system. There is no need to stick to the standard sizes and colour depths as shown in the image either. Icons can be defined at any size (up to a maximum of 128x128 pixels) and at any colour depth.

When you load an icon using the StdPicture object, you only ever get one size: 32x32. It doesn't matter whether there isn't actually a 32x32 image in the .ICO resource, it stretches it as required (usually with horrible consequences). The loaded colour depth is also the always the default system icon colour depth.

There are workarounds for the StdPicture object problem - see my tip "Create a VB Picture from an API Icon Handle" and the article "Get Icons for any File Type" for examples. However, all API methods for loading icons are restricted to only loading icon resources at the system default icon colour depth. On a system with "Show Icons Using All Available Colours" set, you can never load a 16 colour icon if a higher colour depth is available using the API methods.

You can do this if you load the icon natively, though. This article briefly describes the .ICO file format and provides a class which reads icons from .ICO files and Executables and also writes to standard .ICO files. This class is also used in the Icon Explorer project.

The .ICO File Format

ICON files have a defined file format which is described for C programmers in an MSDN article by John Hornick. (Download this icon - there is a picture of him, or at least someone, stored in it at the 118x128 version!).

A .ICO file contains the following information:

  1. A header indicating the type of resource and the number of icons within it.
  2. A series of directory entries providing information about the size and colour depth of the icon, and a pointer to where the icon data starts below it within the file.
  3. A series of icon data structures, containing a BITMAPINFOHEADER structure defining the DIB section used for the icon's image, the palette table (if one is required for the image) and then two byte arrays. The first contains the raw icon image bits in the appropriate DIB format and the second contains the image bits for the mask as a monochrome DIB.

If you are interested in why there are two images in an icon, check out the Transparent Sprite Library article code, which describes the theory of using a mask and an image to implement graphics with transparent areas.

In more detail, the structures to implement the above look like this:

  1. The icon header:
    Private Type ICONDIR
       idReserved As Integer '// Reserved
       idType As Integer '// resource type ( = IMAGE_ICON = 1 for icons)
       idCount As Integer '// how many images?
       ' idEntries() as ICONDIRENTRY array follows.
       ' After that, the icon bits.
    End Type
    
  2. The icon directory entry:
    Private Type ICONDIRENTRY
       bWidth As Byte '// Width of the image
       bHeight As Byte '// Height of the image (times 2)
       bColorCount As Byte '// Number of colors in image (0 if >=8bpp)
       bReserved As Byte '// Reserved
       wPlanes As Integer '// Color Planes
       wBitCount As Integer '// Bits per pixel
       dwBytesInRes As Long '// how many bytes in this resource?
       dwImageOffset As Long '// where in the file is this image
    End Type
    
  3. The icon data structures. These cannot be represented as a straightforward type in VB because the members of the type depend on how many colours are in the image and the size of the icon bitmaps. If you were to write this as a real VB structure, you would have to create at least four different types to accomodate the different number of colour entries. I have worked around this in my icon class by storing this type as a raw byte array and interpreting it as required. However, in pseudo-VB code, the type looks like this:
    Private Type ICONIMAGE
       ' The DIB Header:
       icHeader As BITMAPINFOHEADER
    
       ' The Colour Table If Required:
       If (NumberOfColours<=256 Colours) Then
          icColors(1 to NumberOfColours) as RGBQUAD
       End If
    
       ' The Image Bytes:
       icXOR(0 to (Width * Bits Per Pixel,aligned to a Long Boundary) * Height) As Byte
    
       ' The Mask Bytes
       icAND(0 to (Width/8 aligned to a Long Boundary) * Height) As Byte
    
    End Type
    

Icons In Executable Files

In an executable file, icons are stored in a similar structure, but there are two differences. Firstly, the structures are packed on 2 byte boundaries rather than the normal 4 bytes, and secondly the dwImageOffset member of the ICONDIRENTRY type is replaced with an integer icon ID number. Because the icons are packed on 2 bytes, you cannot write a VB structure which can be directly used to read the format, because VB automatically aligns all its structures on 4 byte boundaries. Instead you have to read all the data in bytes and then copy it into the appropriate structure.

Translating it to VB

To say this wasn't particular easy to translate to VB would be understating the case a bit. I'd describe it here, but it would go on for ever! Instead, download the demonstration project and pick through that. If you'd like to know more about the DIB bitmap format used to store the images, there are two resources here:

The end result is a cFileIcon class. The interface to this class is as follows:

Public Function AddImage(ByVal nWidth As Long, ByVal nHeight As Long, ByVal nColourCount As Long) As Long

Adds a icon to the class with specified width,height and number of colours and returns the index of the new icon. The newly added icon is initialised to be completely black. To modify the icon, you need to add something to the class to set the DIB bits of the XOR (Image) and AND (Mask) DIBs.

Public Sub CloneTo(ByRef cThis As cFileIcon)

Makes a copy of one cFileIcon class to another. The cloned class (cThis) is a completely independent copy.

Public Sub DrawIconImage( ByVal lHDC As Long, ByVal nIndex As Long, Optional ByVal eType As ECFIImageConstants = ecfiImage, Optional ByVal X As Long = 0, Optional ByVal Y As Long = 0, Optional ByVal lWidth As Long = 0, Optional ByVal lHeight As Long = 0, Optional ByVal eRasterOp As RasterOpConstants = vbSrcCopy )

Draws either the Image or the Mask portion of the image to the specified DC, optionally at a specified X,Y position, stretching to a given width and height and drawn using one of the RasterOpConstants.

Public Property Get Filename() As String

Returns the filename of the .ICO file or the Executable from which the icon was loaded.

Public Property Get ImageColourCount(ByVal nIndex As Long) As Long

Returns the number of colours in the icon at 1 based index nIndex.

Public Property Get ImageCount() As Long

Returns the number of icon resources within the currently loaded icon.

Public Property Get ImageHeight(ByVal nIndex As Long) As Long

Returns the height in pixels of the icon at the 1 based index nIndex.

Public Property Get ImageSize(ByVal nIndex As Long) As Long

Returns the number of bytes of the icon at the 1 based index nIndex.

Public Property Get ImageWidth(ByVal nIndex As Long) As Long

Returns the width in pixels of the icon at the 1 based index nIndex.

Public Function LoadIcon(ByVal sFile As String) As Boolean

Loads an icon from a .ICO file. Returns True if it succeeded, or False otherwise. An error will be raised if there is a problem loading the icon.

Public Function LoadIconFromEXE( ByVal sFile As String, Optional ByVal lpID As Long = 0, Optional ByVal lpName As String = "" ) As Boolean

Loads an icon from an executable file (e.g. EXE, OCX, DLL). Specify lpID if the resource has an integer ID, otherwise specify lpName. Returns True if it succeeded, or False otherwise. An error will be raised if there is a problem loading the icon.

Public Function RemoveImage(ByVal nIndex As Long) As Long

Removes the icon resource ID at the 1 based index nIndex.

Public Property Get ResourceID() As Variant

Returns the resource ID that the current icon was extracted from, or Empty if the icon was loaded from a file.

Public Function SaveIcon(Optional ByVal sFileName As String = "") As Boolean

Saves the current icon resources to a .ICO file. If sFileName is not specified, the file that the icon was loaded from will be used. Returns True if successful, or False otherwise. An error will be raised if there is any problem saving the icon.