Obtaining information for shell objects

There is a wealth of additional information for shell objects. Unless you're planning to make some lame text mode exact replica of Norton Commander (RRESPEKT and <g>) you'll need much more than file names and sizes. Icons, overlays, infotips, all contribute to the user experience that will hopefully make shell a warm friendly place to work in.

Topics: Extracting icons | Icon overlays | Infotips | Thumbnails

Extracting object icons

There must be a dozen different ways to get icon information for a particular shell object, well almost. The simplest way is via SHGetFileInfo. It is one of those shell functions that perform all sorts of feats, and extracting icons is one of them. Here's a simple code snippet that shows the icon of a text file in some dialog window:
SHFILEINFO fi;
ZeroMemory(&fi, sizeof(fi));
// despite claims in the docs, you don't need to call CoInitialize first
SHGetFileInfo("c:\\test.txt", FILE_ATTRIBUTE_NORMAL, &fi, sizeof(fi), 
              SHGFI_ICON | SHGFI_LARGEICON );

m_wndIcon.SetIcon(fi.hIcon); // that's a CStatic window in a dialog
DeleteObject(fi.hIcon);

The documentation of SHGetFileInfo is quite complicated on account of all the services offered, even when it comes to icons. In the sample above we asked for a single large icon. Had we used SHGFI_SMALLICON we'd get the small icon instead. Apart from the size, you can ask for other visual effects, like the link overlay, highlighted selected state and so on, using the appropriate constant in uFlags. The results are placed in a SHFILEINFO struct. Note that we have received a copy of the icon, which we have to dispose eventually using DeleteObject.

Instead of reading the icon from an actual file, it is possible to specify SHGFI_USEFILEATTRIBUTES and obtain the icon from shell's cache instead, which is bound to be faster. Hence instead of "c:\\test.txt" we could simply set pszPath to "*.txt" and we would receive the standard icon associated with text files. Docs claim that dwFileAttributes parameter is also used in this case, but to my experience the only attribute that made any difference was FILE_ATTRIBUTE_DIRECTORY, required when obtaining generic folder icons.

The final possible efficiency improvement measure is to ask for the icon index instead of the actual icon. This index maps to the system image list shell maintains for explorer, the desktop, etc. SHGetFileInfo returns a handle to this system resource in such a case. Code that adopts this approach is a bit further down in the section discussing overlay images. The trick is to use SHGFI_SYSICONINDEX instead of SHGFI_ICON.

ADVANCED: Icons in virtual folders
Within virtual folders, regular pathnames aren't very useful. Fortunately, SHGetFileInfo will also retrieve icons for items identified by full PIDLs. To turn this option on just specify SHGFI_PIDL in uFlags and instead of a filename pass the PIDL in pszPath.

ADVANCED: Using COM to extract icons

Icon extraction can take ages some times. Try reading icons from a floppy disc that contains a couple of executable files and a few ".ico" files to know what I mean. The question is, can we do any better than SHGetFileInfo by resorting to low level COM objects?

According to the documentation, the best bet is using IShellIcon, which is exposed by folder objects. This interface is obtained from IShellFolder after directly QueryInterface'ing it for IID_IShellIcon. The benefits stem from the fact that a single interface/object can be used to extract all the icons in a folder, and that makes sense. IShellIcon also knows how to extract custom folder icons specified in a desktop.ini file. But there are a couple of limitations:

I think that the only feasible approach to combat slow icon extraction is doing it on a background thread. As soon as you read the contents of some folder you should make them immediately available to the user, using scrap default icons if necessary. Shell guarantees the following indices in the system image list:

IdxStandard icon
0Blank page, unassociated document
2Application, with extensions .exe .com or .bat
3Generic closed folder

If you probe shell32.dll you'll discover that these icons appear in this very order in the resources. You can take advantage of this knowledge to provide quickly temporary icons, without robbing any of the basic information users need to work with objects. Then at your own time, can extract the real icons using any of the methods described above without irritating the user. That's what explorer does and what 2xExplorer will be doing in the future.

Icon overlays for extra oomph

Overlays add an interesting twist to the whole subject of shell icons. People familiar with image lists will have heard the term before, and surely everybody knows of the little arrow and the little hand symbols indicating that a particular item is a shortcut or a shared folder, respectively.

You can easily determine when an item needs one of these overlays using GetAttributesOf, querying for the SFGAO_LINK and SFGAO_SHARE attributes. But where are the appropriate overlay icons in the system image list? Well, I've got bad news and worse news. The bad news are that in win9x you don't really know where they are; it gets even worse with NT4 where they don't even exist in your "system" image list.

Things have improved with windows 2000, where you can use SHGetIconOverlayIndex to find out about the 1-based index for the standard overlays. Still that leaves many punters unsatisfied though <g>. The workaround in win9x is to use the undocumented fact that the share overlay index is 1 and the link 2. Now we're left with NT4 which is the hardest nut to crack.

The problem with NT4 is that for security reasons SHGetFileInfo won't give you a handle to the real system image list, so as to protect it from "malicious" applications. Instead you get a new empty image list that will be filled in gradually, as you ask for more and more icons in the regular way. This is all too well for security, but mikrosoft must admit that they made a big blunder leaving the standard overlay images out. Article Q192055 offers a "solution" but it is ridiculously complicated, plus it only tackles the link overlay problem — you'll be still at a loss for a share overlay image.

Enter my solution that covers all possible eventualities. The idea is to count the images contained in the system image list returned by SHGetFileInfo, and if it's just the one, indicating an NT4/2000 system, manually add the two overlays, which you've already nicked from shell32.dll (icon ordinals 29 and 30) and placed them in your resources. Here's some code:
HIMAGELIST GetSHImageList(BOOL largeIcons)
{
   SHFILEINFO sfi; // don't care at all about this!
   UINT uFlags = SHGFI_USEFILEATTRIBUTES | SHGFI_SYSICONINDEX |
      (largeIcons ? SHGFI_LARGEICON : SHGFI_SMALLICON);
   HIMAGELIST hshIL = (HIMAGELIST)
      // ask for an icon that would certainly exist
      SHGetFileInfo(_T("*.exe"), FILE_ATTRIBUTE_NORMAL, &sfi,
         sizeof(sfi), uFlags);

   // if this NT, list contains 1 image we asked for + 1 (folder icon)
   if(ImageList_GetImageCount(hshIL) <= 2) {
      // obtain the individual icon size
      int cx, cy;
      if(!ImageList_GetIconSize(hshIL, &cx, &cy))
         cx = cy = largeIcons ? 32 : 16; // provide defaults

      // add the two standard overlays in the list
      int iconID[2] = {IDI_SHAREOVLY, IDI_LINKOVLY};
      for(int i=0; i<2; i++) {
         // load icon (from resources), requesting the appropriate size
         HICON hIcon = (HICON) LoadImage(AfxGetInstanceHandle(),
            MAKEINTRESOURCE(iconID[i]), IMAGE_ICON, cx, cy,
            LR_SHARED/*so that we won't need to delete it*/);

         int idx = -1; // new index of the icon
         if(hIcon) {
            idx = ImageList_ReplaceIcon(hshIL, -1/*add*/, hIcon);
            if(idx != -1)
               if(!ImageList_SetOverlayImage(hshIL, idx, i+1)) 
                  idx = -1; // failure   indicator
         }

         if(-1 == idx) return NULL; // assume hard failure
      }
   }
   // else it's probably the good old Win9x, no actions required

   return hshIL; // remember this is a shared resource, no deleting!
}

This code firstly demonstrates that I do actually know about error correction <g>. On a more serious note, SHGFI_SYSICONINDEX is used to request the system image list. For win9x that's all we need to do, whereas for NT4 we need to patch the (almost) empty image list with the two standard overlay icons. LoadImage is used to load the icons from the resource files, where we've manually inserted them earlier. The easiest way to extract them from shell32.dll is to open this file directly in the Developer Studio, selecting the open as resources option; then you can, erm, borrow whatever you fancy by simply dragging/dropping, in the interest of system-wide uniform user experience, of course <g>.

Note that the icons are added in a particular order, first the share and then the link, as is the case in 9x. Although in general the system image list is a shared resource that should be treated as strictly read-only, in NT you get your own copy so no harm will come about when you add icons in it as above.

Also note that I'm using "raw" ImageList_xxx calls above, since the fellow who wrapped the functionality in MFC's CImageList left out a couple of members, and notably so the ImageList_GetIconSize used for getting the individual icon size. An easier solution would be to hard-code the values 32 for large and 16 for small icons, but robustness would have suffered. I heard that the upcoming windows XP will offer larger icons...

In a real application you would just store the handle to the system image list someplace safe and use it directly, e.g. along with a list control showing folder contents. If used in a list control, make sure to specify LVS_SHAREIMAGELISTS that will avoid its destruction after the window closes.

Usually you'd have another similar call to GetSHImageList() to obtain the handle to the small system image list, if you plan offering all the view modes supported by a list control (details, large icons, etc.). Note that the order of the icons in both small and large image lists is the same, i.e. the index for a text document icon would be 56 in both cases. It is only the icon size that differs.

The underrated Infotip

I thought infotips were totally lame. The principle is good, having some comment associated with an item that would popup when the mouse hovers above it, so that you don't need to insert all this information in the filename. In this way you'd have both manageable filenames and be in a position to figure out more details about specific items.

Unfortunately, in practice the infotip feature has severe limitations. It's very hard for the common layman user to set infotips he/she would prefer, and it is only folders can have their tips tweaked at this higher level through an InfoTip entry in desktop.ini. Programmers on the other hand can do pretty much whatever they wish using the IQueryInfo interface, which can be obtained from some folder object via GetUIObjectOf. But that's hardly the point; it would be just like if we had programmers choosing the filenames on somebody else's computer — a user revolution/uprising would have been inevitable <g>.

So are infotips the total loss they seem to be? Well, it seems that windows 2000 has revived this feature, too, allowing users to specify infotip templates for each individual file type. Some registry tweaking is required, but it is an effort well spent considering all the useful information that could be shown. For example infotips for text files could be added by tweakin the respective ProgId key (HKEY_CLASSES_ROOT\txtfile). Inserting a subkey like InfoTip = Size;Modified;Created would result in infotips for text files showing the requested information. There is a number of standard property names that can be used in infotip templates in this manner.

It gets even better. Except for information that you could have obtained from a standard detailed view, you can go power-up and access all sorts of useful information. For example adding a {F29F85E0-4FF9-1068-AB91-08002B27B3D9},6 token in the template would give you access to the file comment. But what is a file comment I hear many of you ask? Remember when saving a word document you are often presented with a property page for extra information, and you usually can't be bothered to waste any time with it? <g> That's an example of how to set file comments — and other similar information.

The funny number {F29F85E0-4FF9-1068-AB91-08002B27B3D9} is the GUID for the standard OLE document Summary Information Property Set, and the number 6 is the PID (property ID) of the "comment" property within the summary info set. You can see that property sets are registered with a unique GUID just like COM objects. An obvious question is how could we have expected the average user to know all these details; even I had to delve deep into the SDK header files to find out that the PIDSI_COMMENT constant was actually the number 6. It's an impasse, but it also sure is a good idea for a new registry tweaking program, no?

Another issue is that we would prefer to have this kind of additional property information for files other than M/S office compound documents. How about associating comments to simple text files, without tampering with the plain ASCII text format? If we are to believe the docs, apparently the issue is all settled using NTFS-5's parallel stream concept. Thus you could have the regular readMe.txt file, and in parallel you could have a pseudo-file with the same name holding OLE property sets like the summary information mentioned above. It all sounds a bit too good to be true, and I haven't really seen it with my own eyes yet, but this whole thing has surely grasped my attention alright.

Finally folks, I wish to make a disclaimer: I don't have windows 2000 so I haven't really tried any of these special infotips mentioned above. All the information was based on my trusting the online documentation for accuracy. In case they turn out to be false or half-truths, please address your complaints and outrage to Microsoft Ltd.

Extracting thumbnail previews

Thumbnails are cool. You can have a quick preview of the contents of some file without launching the associated application, which when it comes to office documents can save you plenty of time. But how is this feat possible? It turns out that the thumbnail is part of the summary information property set mentioned before for file comments. I wouldn't have thought that you could specify PIDSI_THUMBNAIL in infotip templates, but that's hardly the point.

The point is that Office applications save a preview picture in the compound document, and that's how you can see a snapshot of it without launching the application that has generated it in the first place. Note that whereas PowerPoint will automatically include a thumbnail in the saved file, Word needs some convincing to do so. If you take another look at the document property page you'll see there is this check box towards the bottom which controls whether a preview picture will be saved or not. Thumbnails for other simpler file formats like graphic files (JPG, GIF, etc) are generated by shell extension handlers that read the contents and generate a thumbnail on the fly.

Of course all this information is strictly academic. From a client point of view, all you need to know is that folder objects expose the IExtractImage interface, which has all the methods you need to extract thumbnails out of files, irrespective of exactly how/where they are kept. This is how you do it:
HBITMAP ExtractThumb(LPSHELLFOLDER psfFolder, LPCITEMIDLIST localPidl, 
                     const SIZE* prgSize, DWORD dwRecClrDepth)
{
   LPEXTRACTIMAGE pIExtract = NULL;
   HRESULT hr;
   hr = psfFolder->GetUIObjectOf(NULL, 1, &localPidl, IID_IExtractImage,
                                 NULL, (void**)&pIExtract);
   if(NULL == pIExtract) // early shell version, thumbs not supported
      return NULL;

   OLECHAR wszPathBuffer[MAX_PATH];
   DWORD dwPriority = 0; // IEI_PRIORITY_NORMAL is defined nowhere!
   DWORD dwFlags = IEIFLAG_SCREEN;
   HBITMAP hBmpImage = NULL;
   hr = pIExtract->GetLocation(wszPathBuffer, MAX_PATH, &dwPriority,
                               prgSize, dwRecClrDepth, &dwFlags);
   // even if we've got shell v4.70+, not all files support thumbnails 
   if(NOERROR == hr) hr = pIExtract->Extract(&hBmpImage);
   pIExtract->Release();

   return hBmpImage; // callers should DeleteObject this handle after use
}

This is a generic function that you could use directly in your code. It expects a local PIDL of the item whose thumb is to be extracted, and the container IShellFolder. Earlier samples demonstrate how to enumerate folders for this information. The function also requires the target size of the bitmap and it's color depth, which usually is that of the device context that will receive the image.

We ask the folder object for the IExtractImage of the item using the familiar GetUIObjectOf. This can fail for a number of reasons: for win95 this interface is not supported; for newer shell versions, the item may not have a thumbnail. If nevertheless we manage to obtain a valid pointer, the drill is to call GetLocation first.

GetLocation looks complicated, but down deep it's a lamb in wolf's clothes. You don't need any pathnames or priorities — although you are required to pass valid arguments for these. You just need a successful return code. pdwFlags parameter seems to take loads of different IEIFLAG_xxx values, but most of them are either useless or even incomprehensible: what on earth is IEIFLAG_GLEAM for example? <g> Perhaps the most interesting flag is IEIFLAG_OFFLINE which avoids accessing remote URLs when generating thumbnails for HTML files.

IEIFLAG_ASYNC seems like an interesting option, apparently forcing the extraction in a background thread, but if you look a bit closer you'll realize that it's rather daft, since you'd need to monitor the extraction through IRunnableTask, which basically translates to idle or busy polling in order to determine when the process is over. That kind of polling is just out of the question within applications with message loops. If you must extract thumbnail images asynchronously, the only option is to launch your own background thread and use something like ExtractThumb() there. Note that you must use CoInitialize in such a case, because the new thread doesn't automatically inherit the main application's COM apartment.

After you've crossed the seven seas it's time to call Extract to get the actual bitmap. If this is successful then all you need to do is to present it to the user. The simplest option would be to have some CStatic control in a dialog and use it's SetBitmap member. Note that it is your responsibility to DeleteObject the bitmap handle after you're through with it. Unfortunately, I cannot tell you with certainty whether CStatic will eventually delete the bitmap handle for you, when it gets destroyed. You can blame it all on obscure documentation. 9 out of 10 leaks are caused this way...

Additional information

Q179167 - HOWTO: Retrieve an Icon for a File Class Without a File
Q235630 - PRB SHGetFileInfo Caches Drive Information
Q192055 - PRB: System Image List Does Not Contain Overlay Images
Q115828 - HOWTO: Getting Floppy Drive Information
ARTICLE - Enhance Your User's Experience with New Infotip and Icon Overlay Shell Extensions — MSDN Magazine, March 2000
SAMPLE - Filling the List View Control (Online book by N.Cluts)
Q234310 - DirLV Sample Populates a ListView Control Similar to Explorer
Q192573 - PRB: Image Previews Not Displayed in Windows Explorer




File management Context menu contents