The static method ExtractAssociatedIcon in the System.Drawing.Icon class is typically used in .NET applications to display an icon for a file that is consistent with what the user would see in Windows Explorer. Unfortunately, in spite of its ease-of-use, this built-in method has some serious shortcomings:

  • It always returns a 32×32 icon in preference to a 16×16 icon.
  • It returns a copy of the original, so you cannot use the overloaded constructor for Icon to get a different-sized icon.
  • In some cases, it will return a thumbnail representation of the file instead of the icon for the file type. While this behaviour can be desired, it is not always wanted.
  • The method is unhelpful in situations where the file does not exist, the path is virtual or a generalisation for the file type is desired.

Unfortunately, there is no managed API for achieving the additional functionality, but the process of invoking Win32 functions is quite simple. This article details a utility class for extracting icons, regardless of whether the file exists, and with the ability to select an icon size.

SHGetFileInfo Function

The SHGetFileInfo function is defined in Shell32.dll and can be used to extract an icon from any file system object:

[DllImport("shell32.dll")]
private static extern IntPtr SHGetFileInfo(
    string pszPath,
    uint dwFileAttributes,
    ref SHFILEINFO psfi,
    uint cbSizeFileInfo,
    uint uFlags
);

The SHFILEINFO structure holds various properties of the file, but we are only interested in hIcon, which can be passed to the Icon.FromHandle method. uFlags is the mechanism we use to determine the size of icon we want:

const uint SHGFI_ICON = 0x100;
const uint SHGFI_LARGEICON = 0x0;    // 32x32 pixels
const uint SHGFI_SMALLICON = 0x1;    // 16x16 pixels

These values must be OR’d together, together representing the type of information we want the function to return and the modifier for that behaviour. To abstract this from the user, we can define an enumeration to represent the two practical values of 0x100 and 0x101.

The completed method looks like so:

public static Icon GetIconForFile(string filename, ShellIconSize size) {
    SHFILEINFO shinfo = new SHFILEINFO();
    SHGetFileInfo(filename, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), (uint)size);
    return Icon.FromHandle(shinfo.hIcon);
}

ExtractIconEx Function and the Windows Registry

The above method is already an improvement over ExtractAssociatedIcon, but it still does not cater for a scenario where the file does not exist, is unknown or exists in some virtual location. If the only piece of information we have is the file extension (.pdf, .jpg, etc) then we need another mechanism. Information about the default icons for file types is stored in the Windows Registry, and can be accessed readily using the managed methods in the Microsoft.Win32.Registry class.

Every registered file extension has its own key in HKEY_CLASSES_ROOT. The default value of this key is the class name for the file type (one file type may have many extensions). We can usually get information about the default icon for the file type by examining the value of the key at HKEY_CLASSES_ROOT\[class name]\DefaultIcon. In some cases, there will not be a DefaultIcon node in the key for the class name, however. Often in these situations, there is a value defined at HKEY_CLASSES_ROOT\[class name]\CLSID. If this is the case, the information we want is located at HKEY_CLASSES_ROOT\CLSID\[clsid value]\DefaultIcon.

The format of this information can be given in two ways: The simplest case is a path to an ICO file. In most cases, however, the form is [path],[index] where ‘index’ is either an ordinal position or a resource identifier for the icon to use. These are intended to be passed to a Win32 function called ExtractIconEx:

[DllImport("Shell32.dll")]
public extern static int ExtractIconEx(
    string libName,
    int iconIndex,
    IntPtr[] largeIcon,
    IntPtr[] smallIcon,
    uint nIcons
);

This function extracts icons from resource libraries (i.e. DLL or EXE files containing icons). In our special, limited case, we will request a single icon of a specific size from a known path and index. The complete method looks like so:

public static Icon GetIconForExtension(string extension, ShellIconSize size) {
    RegistryKey keyForExt = Registry.ClassesRoot.OpenSubKey(extension);
    if (keyForExt == null) return null;

    string className = Convert.ToString(keyForExt.GetValue(null));
    RegistryKey keyForClass = Registry.ClassesRoot.OpenSubKey(className);
    if (keyForClass == null) return null;

    RegistryKey keyForIcon = keyForClass.OpenSubKey("DefaultIcon");
    if (keyForIcon == null) {
        RegistryKey keyForCLSID = keyForClass.OpenSubKey("CLSID");
        if (keyForCLSID == null) return null;

        string clsid = "CLSID\\"
            + Convert.ToString(keyForCLSID.GetValue(null))
            + "\\DefaultIcon";
        keyForIcon = Registry.ClassesRoot.OpenSubKey(clsid);
        if (keyForIcon == null) return null;
    }        

    string[] defaultIcon = Convert.ToString(keyForIcon.GetValue(null)).Split(',');
    int index = (defaultIcon.Length > 1) ? Int32.Parse(defaultIcon[1]) : 0;

    IntPtr[] handles = new IntPtr[1];
    if (ExtractIconEx(defaultIcon[0], index,
        (size == ShellIconSize.LargeIcon) ? handles : null,
        (size == ShellIconSize.SmallIcon) ? handles : null, 1) > 0)
        return Icon.FromHandle(handles[0]);
    else
        return null;
}

Callers must watch out for null values, because while every file has an iconic representation under Windows (i.e. the first mechanism never fails), not every file type has a known icon. Depending on the usage, different failsafe/fallback icons may be used in these circumstances.

Example Usage

Icon smallIcon = IconTools.GetIconForFile(
    @"C:\Windows\System32\notepad.exe",
    ShellIconSize.SmallIcon
);

Icon largeIcon = IconTools.GetIconForExtension(".html", ShellIconSize.LargeIcon);

Final Words

The built-in ExtractAssociatedIcon method has its applications, but a more flexible set of mechanisms are required in order to provide users with a complete and consistent visual representation for files and file types. By invoking some Win32 functions, we are able to close this important gap.

Download

There is a newer version of this code available here: IconTools Revisited

15 thoughts on “Building a Better ExtractAssociatedIcon

  1. vote

    Hello, first a big thank you for this post, it is very helpful. One thing that is important to note with these shell functions is that they need to be run from a thread with STA apartment state. This means that you can not easily run this on the thread pool or a background worker. Cheers.

    Reply
  2. vote

    Getting the icon from the registry helped me with performance issues when showing a list of sharepoint files in my application.
    Thanks for posting, works perfectly!!!!!

    Reply
  3. vote

    Thanks a lot Bradley, works perfectly, easy to use and solves the “ExtractAssociatedIcon” deficiencies!

    Reply
    • vote

      It’s right at the bottom of the file, after the IconTools class is declared. Did you forget to copy the entire contents of the file?

      I’ve included the definition (again) below, for the sake of convenience:

      public enum ShellIconSize : uint {
      	SmallIcon = IconTools.SHGFI_ICON | IconTools.SHGFI_SMALLICON,
      	LargeIcon = IconTools.SHGFI_ICON | IconTools.SHGFI_LARGEICON
      }
      
      Reply
  4. vote

    This was an excellent post. Many thanks for your work on this.

    I was able to plugin the IconTools class and it work seamlessly. Just be sure to use the IconTools.cs download link to get all the code.

    Reply
  5. vote

    If you want to get the icon for a file extension you can still use SHGetFileInfo .

    Just pass it the path to the file and use SHGFI_USEFILEATTRIBUTES for uFlags which doesn’t actually check if the file exists.

    No need to piss fart around in the registry.

    Reply
    • vote

      There are three main possibilities that className would be empty for a given file extension:
      1. There is no application installed on the machine that is associated with the extension.
      2. The user account running the code does not have permission to open the registry key.
      3. The operating system treats the extension specially and there is a different path to the key.

      Have a look in the registry editor to see if there is a matching key for the file extension. If not, then (1) is true. If there is, and the key’s default value contains the class name, then (2) may be true. Otherwise, (3) is the likely outcome. Expand the key and see if there are any other useful values that point to the application or icon.

      Reply
      • vote

        Thanks! It was mostly 1 but for one extension I ended up having to pull the icon from the .exe that opens it.
        I don’t know if there’s a way to do that automatically if needed but if there is it might be a neat thing to include!

        Reply
  6. vote

    I was checking your function and theres something that I think is missing, to my understanding you have to destroy the handle using DestroyIcon.

    Reply
  7. Pingback: IconTools Revisited | Brad Smith's Coding Blog

  8. vote

    Hi,

    What about reading Icons from Byte Array file stream? I mean in case if we have no file and saving this file stream to a temporary file is not wanted.
    This may be when reading file stream from a database, i.e reading .exe or .dll file stream from MSI package “Icons” table.

    Reply
    • vote

      That doesn’t really fall within the scope of my article; I wrote about how to obtain icons for a particular file type (whether a file exists or not). If you already know where the icon is located then you can simply load it using the methods on the Icon class.

      Reply

Leave a reply to Michael Cancel reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required