3

Given a file type (e.g. .txt) how can i get the:

  • path
  • index

to the file type's associated icon path and index, e.g.:

I want to convert .txt into:

  • Path: %SystemRoot%\system32\imageres.dll
  • Index: -102

With this information i can then extract the icon (e.g. using SHDefExtractIcon).

Background

Every type of file in Windows is registered in the registry. When an icon is assocated with the file, it is specified as a Path to the file that contains the icon, and the index of the icon resource (or as a resource ID if the index is negative).

Using .txt file as an example, the associated DefaultIcon is:

%SystemRoot%\system32\imageres.dll,-102

ExtractAssociatedIcon

First there was the Win API function ExtractAssociatedIcon:

Retrieves a handle to an indexed icon found in a file or an icon found in an associated executable file.

The idea is that you pass it the path, and the index, and it will go get the icon for you:

String iconPath = "%SystemRoot%\system32\imageres.dll";
Word iIcon = -102;

HICON ico = ExtractAssociatedIcon(0, iconPath, iIcon);

That works only when you already know the path and index of the icon you want.

Fortunately, ExtractAssociatedIcon is also able to tell you the path and index for a icon's file:

If the function cannot obtain the icon handle from that file, and the file has an associated executable file, it looks in that executable file for an icon.

Correctly calling the function in this case is a bit tricky, as it will modify your supplied buffer (causing buffer overruns if you didn't pad your buffer to be long enough):

String iconPath = "C:\Example.txt" + StringOfChar(\0, 32767); //pad the InOut buffer
Word iIcon = 0;

HICON ico = ExtractAssociatedIcon(0, iconPath, iIcon);
DestroyIcon(ico);

When the function returns:

  • iconPath: %SystemRoot%\system32\imageres.dll
  • iIcon: -102

Why didn't i just use the HICON returned by ExtractAssociatedIcon? Because ExtratAssociatedIcon doesn't allow me to specify the size of the Icon i want. It returns the "Shell large icon" and that's it.

Also, the only way ExtractAssociatedIcon can perform it's heroic efforts of looking up by file type is if the file actually exists. If the specified file does not exist (which it doesn't - since there is no foo.txt), the function fails.

SHDefExtractIcon

Enter SHDefExtractIcon. It is able to extract any size of icon i want, you just have to pass it the path and index of the icon resource:

String iconFile = "%SystemRoot%\system32\imageres.dll";
Int32 iIndex = -102;
HICON hLargeIcon;

if (SHDefExtractIcon(iconFile, iIndex, 0, out hLargeIcon, null, 256) == S_OK)
   return hLargeIcon

The only problem is that i have to get the associated path and index for a file type already. And SHDefExtractIcon, unlike ExtractAssociatedIcon, will not perform the heroic lookup for you.

For that i have to perform the lookup myself; which is my question.

Buggy Registry Spelunking

My first attempt is to read the contract of file associations from the other side. I know how default icons are registered, and i can go in reverse.

  • convert the .ext to the assocated ProgID:

    HKEY_CLASSES_ROOT/.ext
        (default) = [ProgID]
    
  • Lookup the DefaultIcon under the [ProgID]

    HKEY_CLASSES_ROOT/[progID]/DefaultIcon
        (default) = [path],[index]
    

In my case:

HKEY_CLASSES_ROOT/.txt
   (default) = txtfile

HEKY_CLASSES_ROOT/txtfile/DefaultIcon
   (default) = "%SystemRoot%\system32\imageres.dll,-102"

This is the approach used by the code behind this accepted Stackoverflow answer to the same question:

  • convert ext to progID
  • lookup progID's DefaultIcon key

Assuming the ext exists, and the progID exists, and the DefaultIcon exists, and the path exists, and i can parse the path, it's an incorrect unsupported answer. There are edge cases that the accepted code does not handle1.

I'd like the Windows API supported way to perform the mapping from .ext to

  • path
  • index

SHGetFileInfo

There is a handy function SHGetFileInfo. It's handy because the filename doesn't need to actually exist. If you pass it the SHGFI_USEFILEATTRIBUTES flag, It means:

Do not access the disk. Pretend that the file/directory exists, and that its file attributes are what I passed as the dwFileAttributes parameter. Do this regardless of whether it actually exists or not.

This is good:

SHELLFILEINFO sfi;
DWORD res = SHGetFileInfo("foo.txt", 
      FILE_ATTRIBUTE_NORMAL,
      ref shellFileInfo,
      sizeof(shellFileInfo),
      SHGFI_ICON | SHGFI_LARGEICON | SHGFI_SHELLICONSIZE | SHGFI_USEFILEATTRIBUTES);

if (res <> 0)
   return shellFileInfo.hIcon;

The only problem is that i cannot specify the icon size i want. I am limited to the sizes of icons that the shell decides it wants to use.

IExtractImage

IExtractImage is nice:

  • it can return the [path],[index] of an associated icon
  • i can specify a desired size

Unfortunately it requires a file to actually exist (it has to be something that exists in the shell namespace). When i have a file-type only i can't use IExtractImage

IThumbnailProvider

IThumbnailProvider, introduced with Windows Vista, is the modern replacement for IExtractImage:

Windows Vista IThumbnailProivder is new for Vista and replaces IExtractImage. Vista still supports IExtractImage but lacks the ability to return the image type (alpha or not).

IThumbnailProvider also lets me supply the desired icon size. Excellent!

IThumbnailProvider normally requires a file to exist in the shell namespace. But that's only because the Shell API is the only supported way to get ahold ("bind") to the IThumbnailProvider shell interface exposed by a file type.

Fortunately i can perform the same horrible hacks i used above, and crawl the registry manually:

HKEY_CLASSES_ROOT/.ext/ShellEx/[InterfaceID]
   (default) = [ClassID]

if it doesn't exist:

HKEY_CLASSES_ROOT/.ext
   (default) = [ProgID]

HKEY_CLASSES_ROOT/[ProgID]/ShellEx/[InterfaceID]
   (default) = [ClassID]

In the case of my .avi file:

HKEY_CLASSES_ROOT/.avi/ShellEx/{e357fccd-a995-4576-b01f-234630154e96}
   (default) = "{9DBD2C50-62AD-11D0-B806-00C04FD706EC}"

And now i'm off to the races with a CLSID!

Unfortunately it ends there, as IThumbnailProvider requires a file. More precisely, it requires IInitializeWithStream.

I don't have a stream. I don't have a file. I only have the notion of a file type.

AssocQueryString

Perhaps AssocQueryString can help me? I don't actually know - it's a beast of a function. And i can't make head nor tails of it.

The question

Given a file type (e.g. "x.txt"), how can i get the associated icon:

  • path
  • index

so i may extract the icon of my own desired size (likely using SHDefExtractIcon)?

Footnotes

1 exefile and %1

Community
  • 1
  • 1
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219

1 Answers1

1

The answer is that i was very close:

  • Use SHGetFileInfo with the SHGFI_ICONLOCATION flag to get the default icon Path and Index
  • Use SHDefExtractIcon to extract the default icon at Path,Index at the desired size

Or in function form:

HICON GetFileTypeDefaultIcon(String filename, Int32 iconSizePx)
{
    //Filename is anything like "a.txt", "foo.xml", "x.zip"
    //The file doesn't have to exist, but it can't be an invalid 
    //filename (e.g. "???.txt" is no good)

    //Use SHGetFileInfo to get the path and index of our file type's icon
    SHFILEINFO sfi;

    //SHGFI_IconLocation means get me the path and icon index
    //SHGFI_UseFileAttributes means the file doesn't have to exist
    DWORD_PTR res := SHGetFileInfo(
            filename,
            FILE_ATTRIBUTE_NORMAL,
            ref sfi,
            sizeof(sfi),
            SHGFI_ICONLOCATION or SHGFI_USEFILEATTRIBUTES);

   if (res = 0) //"nonzero if successful"
      return 0;

   //The path and index are stuffed into the ShellFileInfo structure
   String iconPath := sfi.szDisplayName;
   Int32 iconIndex := sfi.iIcon;

   //Now that we know the path and index, we can use SHDefExtractIcon
   HICON largeIcon;
   iconSizePx = iconSizePx and 0xFFFF; //preferred large icon size is in LOWORD 16-bits

   HRESULT hr := SHDefExtractIcon(iconPath, iconIndex, 0, 
      out largeIcon, null, iconSizePx); 
   if (hr <> S_OK) 
      return 0;

   return largeIcon;
}

Stop spellunking for shell objects

I also figured out that there is a 20 year old Windows function that can perform the crawling of the registry for shell extensions. It also handles the cases that i missed - because it is the canonically correct way.

Just to document and explain shell classes:

File types have a ShellEx key, with {guid} subkeys. Each {guid} key represents a particular InterfaceID.

There are a number of standard shell interfaces that can be associated with a file type:

  • {BB2E617C-0920-11d1-9A0B-00C04FC2D6C1} IExtractImage
  • {953BB1EE-93B4-11d1-98A3-00C04FB687DA} IExtractImage2
  • {e357fccd-a995-4576-b01f-234630154e96} IThumbnailProvider
  • {8895b1c6-b41f-4c1c-a562-0d564250836f} IPreviewHandler

If i want to find, for example, the clsid of the IThumbnailProvider associated with a .jpg file, i would look in:

HKEY_CLASSES_ROOT/.jpg/ShellEx/{e357fccd-a995-4576-b01f-234630154e96}
   (default) = [clsid]

But that's not the only place i could look. I can also look in:

HKEY_CLASSES_ROOT/.jpg
   (default) = jpgfile
HKEY_CLASSES_ROOT/jpgfile/ShellEx/{e357fccd-a995-4576-b01f-234630154e96}
   (default) = [clsid]

But that's not the only place i could look. I can also look in:

HKEY_CLASSES_ROOT/SystemFileAssociations/.jpg/ShellEx/{e357fccd-a995-4576-b01f-234630154e96}
   (default) = [clsid] 

But that's not the only place i could look. I can also look in:

HKEY_CLASSES_ROOT/SystemFileAssociations/jpegfile/ShellEx/{e357fccd-a995-4576-b01f-234630154e96}
   (default) = [clsid]

But that's not the only place i could look. If i think the file is an image, i can also look in:

HKEY_CLASSES_ROOT/SystemFileAssociations/image/ShellEx/{e357fccd-a995-4576-b01f-234630154e96}
   (default) = [clsid]

How did i find these locations? Did i only follow documented and supported locations? No, i spied on Explorer using Process Monitor as it went hunting for an IThumbnailProvider.

So now i want to use a standard shell interface for a file-type myself. This means that i have to crawl the locations. But why crawl these locations in an undocumented, unsupported way. Why incur the wrath from the guy from high atop the thing? Use AssocQueryString:

Guid GetShellClassIDForFileType(String fileExtension, Guid interfaceID)
{
    //E.g.:
    //   String fileExtension = ".jpg"
    //   Guid   interfaceID   = "{BB2E617C-0920-11d1-9A0B-00C04FC2D6C1}"; //IExtractImage

    //The interface we're after - in string form
    String szInterfaceID := GuidToString(interfaceID);

    //Buffer to receive the clsid string
    DWORD bufferSize := 1024; //more than enough to hold a 38-character clsid
    SetLength(buffer, bufferSize);

    HRESULT hr := AssocQueryString(
          ASSOCF_INIT_DEFAULTTOSTAR, 
          ASSOCSTR_SHELLEXTENSION, //for finding shell extensions
          fileExtension, //e.g. ".txt"
          szInterfaceID, //e.g. "{BB2E617C-0920-11d1-9A0B-00C04FC2D6C1}"
          buffer,        //will receive the clsid string
          @bufferSize);
   if (hr <> S_OK) 
      return Guid.Empty;

   Guid clsid;
   HRESULT hr = CLSIDFromString(buffer, out clsid);
   if (hr <> NOERROR) 
      return Guid.Empty;

   return clsid;
}
Community
  • 1
  • 1
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219