How do I extract an icon at a nonstandard size if IExtractIcon::Extract tells me to go jump in a lake?


Commenter Ivo notes that if you ask IExtract­Icon::Extract to extract an icon at a particular size, the function can return S_FALSE which means “Go jump in a lake do it yourself.” But how can you do it yourself? The Extract­Icon and Extract­Icon­Ex functions don’t let you specify a custom size, and Load­Image doesn’t work with icon indices (only resource IDs).

The SH­Def­Extract­Icon function comes to the rescue. This takes all the parameters of IExtract­Icon::Extract (plus a bonus flags parameter), and it will actually do an extraction.

Let’s extract an icon from Explorer at 48×48, just for illustration. As usual, start with our scratch program, then make these changes:

#include <shlobj.h>

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
  HICON hico;
  if (SHDefExtractIcon(TEXT("C:\\Windows\\Explorer.exe"),
                       1, 0, &hico, NULL, 48) == S_OK) {
    DrawIconEx(pps->hdc, 0, 0, hico, 0, 0, 0, NULL, DI_NORMAL);
    DestroyIcon(hico);
  }
}

Run the program, and observe that it draws the second icon from Explorer (whatever it is) at a size of 48×48.

Comments (13)
  1. Ivo says:

    Thanks, Raymond.

    Now a follow-up question. For some items (like C:WindowsSystem32doskey.exe) IExtractIcon::GetIconLocation returns the flag GIL_NOTFILENAME – "whatever you do, don't extract the icon yourself". The location is "C:WindowsSystem32doskey.exe" and the index is 0x00d7cc00. IExtractIcon::Extract still returns S_FALSE – "do it yourself". I'm getting mixed signals here. What shall I do?

    Just for fun, I tried passing the location+index to ExtractIcon, ExtractIconEx, SHDefExtractIcon, even SHExtractIconsW. All returned S_FALSE as expected.

    So how do I get the icon for doskey.exe? In a custom size, if possible :)

  2. ErikF says:

    @Ivo: Are you sure that there's an icon to be had for these files? I dumped the PE structure for doskey.exe and it seems to have three resources: "MUI", Version Information and "24".

  3. kinokijuf says:

    Doskey does not contain any icons.

  4. ZLB says:

    if(icon->Extract() == S_FALSE);

       sudo icon->Extract(); //No, you go jump in a lake!

  5. MNGoldenEagle says:

    @Ivo: The icon you're looking for is in C:WindowsSystem32imageres.dll, at least on Windows 7.  I'm pretty sure Microsoft makes no guarantees that the same icon will be made available in the same DLL in any future versions of Microsoft, or that the DLL will continue to exist in future versions (case in point: MORICONS.DLL).

  6. Joshua says:

    > it downscales the jumbo icon even if there is already an icon of the requested size.

    There's the icon display bug I never could figure out.

  7. ErikF says:

    The documentation for GetIconLocation ( msdn.microsoft.com/…/bb761852(v=vs.85).aspx ) specifies that if it returns S_FALSE that you should use a default icon (probably gotten from calling GetIconLoacation with GIL_DEFAULTICON in uFlags); my guess is that they assume that you grab that first and show it until you load the actual icon. If you look at Explorer loading the contents of a folder you'll see the default application icon shown until the actual icon is loaded, for example.

  8. Ivo says:

    @ErikF – but it doesn't – GetIconLocation returns S_OK. And gives what looks like a valid location and index.

  9. Ivo says:

    @MNGoldenEagle: sure, but do you expect me to hard-code that "if file==doskey.exe then go look in imageres.dll"?

    @kinokijuf: Text files also don't contain any icons but GetIconLocation returns the default text document icon. For EXEs with no icons I expect to get the default application icon, like the one you see in Explorer.

    If GetIconLocation doesn't know the location of the icon it should do the honorable thing and return E_FAIL instead of making things up.

    There must be a (not very complicated) algorithm for getting an icon for shell item. According to the documentation that algorithm is: call GetUIObjectOf to get IExtractIcon, then call GetIconLocation, then call Extract, and if Extract happens to return S_FALSE call ExtractIcon.

    The IExtractIcon API works quite well on paper. GetIconLocation is supposedly fast, and if the extraction can take a long time it returns E_PENDING, so you know to run Extract in a background thread. You can also use the location as a key in your icon cache so you don’t have to extract the same icon multiple times (like for each .txt file).

    In practice it is not that simple:

    * Some handlers only implement IExtractIconA and not IExtractIconW (what is this, 1998?)

    * If you want a custom size you can't use ExtractIcon (Raymond just explained what to do)

    * In the doskey.exe case it just fails to provide the correct icon and even lies about it

    * The Game Explorer implementation of IExtractIcon::Extract crashes if you don't ask for both the large and the small icon, even though the docs say the parameters are optional

    * The DeviceCenter namespace extension decides to unload itself after a while even if you have valid IShellFolder reference. Trying to extract the icon after that will crash

    There are alternatives, like SHGetFileInfo and IShellItemImageFactory but they are less than optimal. They don’t separate the location from the extraction, so you can’t do any caching or background processing. SHGetFileInfo doesn’t support custom sizes, and while IShellItemImageFactory does, it downscales the jumbo icon even if there is already an icon of the requested size. In Windows 8.1 it also switched from using HALFTONE mode to COLORONCOLOR mode, which can produce some quite ugly icons.

    Bottom line, extracting an icon from a shell item is ridiculously tricky to get to work in all cases (if you want caching, performance, high quality, and custom sizes). There ought to be a better way.

  10. Jerome says:

    In the case of something like doskey.exe, are you sure you really want the icon? Maybe the shell thumbnail is what you are looking for? I've done this in .Net with a little bit of pinvoke, using IShellItemImageFactory for Windows 7/8.

    Essentially the pattern is, check if Environment.OSVersion.Version.Major >= 6 (and if it isn't, use IShellItemImageFactory instead.)

    Then use SHCreateItemFromParsingName to get out parameter of type IShellItem, cast to IShellItemImageFactory, and call GetImage.

  11. Jerome says:

    Oops… bad copy and paste. It should have read, for older OS, use IExtractImage instead.

  12. Ivo says:

    @Jerome – yes, I know about IShellItemImageFactory, but it doesn't help with the reuse of icons (it gives me separate icons for each .txt file, for example) and also it has poor quality for icons above 32×32 and below 256×256.

  13. Jerome says:

    @Ivo – Pity… I was hoping it might help, but I have only used it for thumbnails, not icons… and I always get them at 256×256, then cache them. Works fine for high quality thumbnails; the only issue I had was directory thumbnails come back overlaid on a white background, and I had to use a flood-fill hack to remove the white background from the edges to recreate a transparent image.

Comments are closed.