Extracting system icons from Win32

Overwiew

Hi, everyone! :)

So I was working on this project recently where I needed to display file lists with their fancy system icons. Sounds pretty straightforward, right? Well, it is - until you need to handle network paths or grab different icon sizes beyond the basic 32×32 pixels. Then things get… interesting.

The built-in System.Drawing.Icon.ExtractAssociatedIcon() method works great for local files and standard 32×32 icons, but it completely chokes on UNC paths and doesn’t give you any size flexibility. Since I needed to support network paths and display multiple icon sizes (including those gorgeous jumbo icons), I had to dive into the Win32 API rabbit hole.

If you know a cleaner way to do this, seriously, hit me up! 😅

TL;DR - Show me the code!

Let’s cut to the chase and see what we’re building here. The final API is beautifully simple - just three lines to get any icon at any size:

Given a file path and your desired icon size, boom! 💥 System icon delivered!

var filename = "\\myNetworkResource\Folder\SampleDocument.pdf";
var size = IconSizeEnum.ExtraLargeIcon;
var image = GetFileImageFromPath(filename, size);

The Win32 plumbing

Alright, here’s where things get spicy. We need to import a bunch of Win32 functions and define some structs. It’s a bit verbose, but hey, that’s the price of talking directly to Windows. Just dump all this into a class and you’re good to go:

[ComImportAttribute()]
[GuidAttribute("46EB5926-582E-4017-9FDF-E8998DAA0950")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
private interface IImageList
{
    [PreserveSig]
    int Add(
        IntPtr hbmImage,
        IntPtr hbmMask,
        ref int pi);

    [PreserveSig]
    int ReplaceIcon(
        int i,
        IntPtr hicon,
        ref int pi);

    [PreserveSig]
    int SetOverlayImage(
        int iImage,
        int iOverlay);

    [PreserveSig]
    int Replace(
        int i,
        IntPtr hbmImage,
        IntPtr hbmMask);

    [PreserveSig]
    int AddMasked(
        IntPtr hbmImage,
        int crMask,
        ref int pi);

    [PreserveSig]
    int Draw(
        ref IMAGELISTDRAWPARAMS pimldp);

    [PreserveSig]
    int Remove(
        int i);

    [PreserveSig]
    int GetIcon(
        int i,
        int flags,
        ref IntPtr picon);
};
private struct IMAGELISTDRAWPARAMS
{
    public int cbSize;
    public IntPtr himl;
    public int i;
    public IntPtr hdcDst;
    public int x;
    public int y;
    public int cx;
    public int cy;
    public int xBitmap;
    public int yBitmap;
    public int rgbBk;
    public int rgbFg;
    public int fStyle;
    public int dwRop;
    public int fState;
    public int Frame;
    public int crEffect;
}
private struct SHFILEINFO
{
    public IntPtr hIcon;
    public int iIcon;
    public uint dwAttributes;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 254)]
    public string szDisplayName;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string szTypeName;
}

private const int SHGFI_SMALLICON = 0x1;
private const int SHGFI_LARGEICON = 0x0;
private const int SHIL_JUMBO = 0x4;
private const int SHIL_EXTRALARGE = 0x2;
private const int WM_CLOSE = 0x0010;

public enum IconSizeEnum
{
    SmallIcon16 = SHGFI_SMALLICON,
    MediumIcon32 = SHGFI_LARGEICON,
    LargeIcon48 = SHIL_EXTRALARGE,
    ExtraLargeIcon = SHIL_JUMBO
}

[DllImport("user32")]
private static extern
    IntPtr SendMessage(
    IntPtr handle,
    int Msg,
    IntPtr wParam,
    IntPtr lParam);

[DllImport("shell32.dll")]
private static extern int SHGetImageList(
    int iImageList,
    ref Guid riid,
    out IImageList ppv);

[DllImport("Shell32.dll")]
private static extern int SHGetFileInfo(
    string pszPath,
    int dwFileAttributes,
    ref SHFILEINFO psfi,
    int cbFileInfo,
    uint uFlags);

[DllImport("user32")]
private static extern int DestroyIcon(
    IntPtr hIcon);

Putting it all together

In this code we’re using several API calls to the functions we declared before. You can paste all this code in the same class. Here’s the tricky part:

public static System.Drawing.Bitmap GetFileImageFromPath(
    string filepath, IconSizeEnum iconsize)
{
    IntPtr hIcon = IntPtr.Zero;
    if (System.IO.Directory.Exists(filepath))
        hIcon = GetIconHandleFromFolderPath(filepath, iconsize);
    else
        if (System.IO.File.Exists(filepath))
            hIcon = GetIconHandleFromFilePath(filepath, iconsize);
    return GetBitmapFromIconHandle(hIcon);
}

private static IntPtr GetIconHandleFromFilePath(string filepath, IconSizeEnum iconsize)
{
    var shinfo = new SHFILEINFO();
    const uint SHGFI_SYSICONINDEX = 0x4000;
    const int FILE_ATTRIBUTE_NORMAL = 0x80;
    uint flags = SHGFI_SYSICONINDEX;
    return GetIconHandleFromFilePathWithFlags(filepath, iconsize, ref shinfo, FILE_ATTRIBUTE_NORMAL, flags);
}

private static IntPtr GetIconHandleFromFolderPath(string folderpath, IconSizeEnum iconsize)
{
    var shinfo = new SHFILEINFO();

    const uint SHGFI_ICON = 0x000000100;
    const uint SHGFI_USEFILEATTRIBUTES = 0x000000010;
    const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
    uint flags = SHGFI_ICON | SHGFI_USEFILEATTRIBUTES;
    return GetIconHandleFromFilePathWithFlags(folderpath, iconsize, ref shinfo, FILE_ATTRIBUTE_DIRECTORY, flags);
}

private static System.Drawing.Bitmap GetBitmapFromIconHandle(IntPtr hIcon)
{
    if (hIcon == IntPtr.Zero) return null;
    var myIcon = System.Drawing.Icon.FromHandle(hIcon);
    var bitmap = myIcon.ToBitmap();
    myIcon.Dispose();
    DestroyIcon(hIcon);
    SendMessage(hIcon, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    return bitmap;
}

private static IntPtr GetIconHandleFromFilePathWithFlags(
    string filepath, IconSizeEnum iconsize,
    ref SHFILEINFO shinfo, int fileAttributeFlag, uint flags)
{
    const int ILD_TRANSPARENT = 1;
    var retval = SHGetFileInfo(filepath, fileAttributeFlag, ref shinfo, Marshal.SizeOf(shinfo), flags);
    if (retval == 0) throw (new System.IO.FileNotFoundException());
    var iconIndex = shinfo.iIcon;
    var iImageListGuid = new Guid("46EB5926-582E-4017-9FDF-E8998DAA0950");
    IImageList iml;
    var hres = SHGetImageList((int)iconsize, ref iImageListGuid, out iml);
    var hIcon = IntPtr.Zero;
    hres = iml.GetIcon(iconIndex, ILD_TRANSPARENT, ref hIcon);
    return hIcon;
}

How this wizardry works

Here’s the step-by-step breakdown of what’s happening under the hood:

First stop: SHGetFileInfo() - This little gem gives us the icon index from Windows’ system image list. Think of it as asking “Hey Windows, what’s the index number for this file’s icon?”

Then we’ve to perform a second call to the SHGetImageList function that receives an output parameter with an IImageList structure, which is modified within the function.

Once we have that COM interface, we only need to call its GetIcon method, passing a parameter with the desired size, and obtaining a handle to the icon by reference.

Finally, using the handle we can create an Icon and then convert it to a Bitmap or BitmapSource:

Tip: Don’t forget to destroy the resources (Icon) when working with the Win32 API!

For better use I’ve also created an enumeration IconSizeEnum (see types declaration) with the different flags values as well:

public enum IconSizeEnum
{
    SmallIcon16 = SHGFI_SMALLICON,
    MediumIcon32 = SHGFI_LARGEICON,
    LargeIcon48 = SHIL_EXTRALARGE,
    ExtraLargeIcon = SHIL_JUMBO
}

Testing it out

Want to see it in action? Throw together a quick WinForms test app with a PictureBox, a Label, and a Button. Drop this code into your button’s click handler:

var size = IconSizeEnum.ExtraLargeIcon;
var ofd = new OpenFileDialog();
if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
    labelFilePath.Text = ofd.FileName;
    pictureBox1.Image = YourClassNameHere.GetFileImageFromPath(ofd.FileName, size);
}

Bonus: This works perfectly with UNC network paths too! No more headaches with \\server\share\file.txt paths.

And that’s it! You’ve now got a robust way to extract system icons at any size, including support for network paths. The Win32 API might be a bit verbose, but once you wrap it up like this, it’s actually pretty elegant to use.

Happy coding! 🚀

Extracting system icons from Win32

Author

Lluis Franco

Publish Date

04 - 16 - 2014