Version 33 of Retrieve file icon using the Win32 API

Updated 2016-04-11 10:29:37 by MJ

MJ - To get the icon windows explorer displays for a file requires quite a lot of registry searching to determine the source of the icon (dll, exe, ico) and the index in that file. Even then it is not guaranteed that the correct icon will be returned, because Windows explorer seems to use several mechanisms to determine the correct icon.

The Win32 API function SHGetFileInfo [L1 ] will reliably retrieve the correct icon from a file (note that any file is supported, not just exe, dll and ico files).

The C extension below provides a new Tcl command [::shellicon::get ?options? fileName] which will create a Tk image from the icon associated to fileName. The options are flags that modify the appearance of the icon:

 -large get the large version of the icon (small is the default)
 -selected get the appearance of the icon when it is selected
 -open get the icon that is shown when the file is opened (e.g. an opened folder)

Screenshot

An example of all the different icon types, note that the link overlay for shortcuts will be returned as well.

http://mjanssen.nl/files/shellicon.png

Example

 # dll provides the package shellicon
 load shellicon0.1.dll

 label .l1 -image [::shellicon::get c:/Windows]
 label .l2 -image [::shellicon::get -large  c:/Windows]
 label .l3 -image [::shellicon::get -selected c:/Windows]
 label .l4 -image [::shellicon::get -large -selected c:/Windows]
 label .l5 -image [::shellicon::get -open c:/Windows]
 label .l6 -image [::shellicon::get -large -open c:/Windows]
 label .l7 -image [::shellicon::get -open -selected c:/Windows]
 label .l8 -image [::shellicon::get -large -open -selected c:/Windows]

 grid .l1 .l2 .l3 .l4 .l5 .l6 .l7 .l8

 catch {console show}

Build

To build with gcc use:

 gcc shellicon.c -lgdi32 -ltclstub85 -ltkstub85 -I/c/Tcl/include -L/c/Tcl/lib\
 -DUSE_TCL_STUBS -DUSE_TK_STUBS -shared -o shellicon0.1.dll

A stubs enabled dll for >8.5 can be downloaded from [L2 ].

AlanG - 2008-01-22 I can't follow the link above - 404 no such file. I tried various other versions of the file with no luck, could someone update the link please?

MJ - URL changed to correct location.

Source

 #include <tcl.h>
 #include <tk.h>
 #include <windows.h>
 #include <shellapi.h>
 #include <stdio.h>

 static int GetIcon_Cmd(ClientData cdata, Tcl_Interp *interp, int objc,  Tcl_Obj * CONST objv[]) {
   SHFILEINFOW shfi;
   ICONINFO iconInfo ;
   BITMAP bmp;
   long imageSize ;
   char * bitBuffer , * byteBuffer ;
   int i, index;
   int result, hasAlpha;
   const char * image_name;
   Tk_PhotoHandle photo;
   Tk_PhotoImageBlock block;
   const char * file_name;
   int bitSize;
   unsigned int uFlags;

   static CONST char *options[] = {
     "-large", "-open", "-selected", NULL};
   enum IOption {
     ILARGE, IOPEN, ISELECTED
   };

   if (objc < 2) {
     Tcl_WrongNumArgs(interp,1,objv,"?options? fileName");
     return TCL_ERROR;
   }

   /* SHGFI_ICON == SHGFI_LARGEICON so large is the default, select small instead
      then remove the flag if -large is specified
    */

   uFlags = SHGFI_ICON | SHGFI_SMALLICON;

   for (i=1 ; i < objc-1 ; i++) {
     result = Tcl_GetIndexFromObj(interp, objv[i], options, "option", 0,
                                 (int *) &index);
     if (result != TCL_OK) {
       return result;
     }
     switch (index) {
     case ILARGE:
       /* setting LARGE is equivalent to unsetting SMALL */
       uFlags ^= SHGFI_SMALLICON;
       break;
     case IOPEN:
       uFlags |= SHGFI_OPENICON;
       break;
     case ISELECTED:
       uFlags |= SHGFI_SELECTED;
       break;
     default:
       Tcl_Panic("option lookup failed");
     }
   }

   /* Normalize the filename */
   file_name = Tcl_FSGetNativePath(objv[objc-1]);
   if (file_name == NULL) {
     return TCL_ERROR;
   }

   result = SHGetFileInfoW(
                         (LPCWSTR) file_name,
                         0,
                         &shfi,
                         sizeof(SHFILEINFO),
                         uFlags
                         );

   if (result == 0) {
     WCHAR msg[255];
     int l;
     Tcl_SetResult(interp, "failed to load icon: ",NULL);
     FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,0,GetLastError(),0,msg,255,0);

     /* lose the newline */
     l = 0;
     while (msg[l]!='\r' && msg[l]!='\n' && msg[l]!='\0') {
       l++;
     }
     msg[l]='\0';
     Tcl_AppendResult(interp, msg,NULL);
     return TCL_ERROR;
   }

   GetIconInfo(shfi.hIcon, &iconInfo);

   result = GetObject(
                     iconInfo.hbmMask,
                     sizeof(BITMAP),
                     (void *)&bmp
                     );

   bitSize = bmp.bmWidth * bmp.bmHeight * bmp.bmBitsPixel / 8;

   bitBuffer = ckalloc(bitSize);
   GetBitmapBits(iconInfo.hbmMask,bitSize,bitBuffer);

   result = GetObject(
                     iconInfo.hbmColor,
                     sizeof(BITMAP),
                     (void *)&bmp
                     );

   imageSize = bmp.bmWidth * bmp.bmHeight * bmp.bmBitsPixel / 8;
   byteBuffer = ckalloc(imageSize);
   GetBitmapBits(iconInfo.hbmColor,imageSize,byteBuffer);

   /* Do some mask and Alpha channel voodoo, because not all Icons define an alpha channel
      and MS has decided to make completely transparent the default in that case, AAARGGH
      Might be some bit masking I am missing here though.
    */

   hasAlpha = 0;
   for (i = 0 ; i < imageSize ; i+=4) {
     if (byteBuffer[i+offsetof(RGBQUAD,rgbReserved)]!=0) {
       hasAlpha = 1;
       break;
     }
   }

 #define BIT_SET(x,y) (((x) >> (8-(y)) ) & 1 )

   for (i=0;i<bitSize;i++) {
     if (hasAlpha) break;
     // if (i%2==0) {fprintf(stderr,"\n");}
     int bit = 0;
     for (bit=0; bit < 8 ; bit++) {
       if (BIT_SET(bitBuffer[i],bit)) {
        // fprintf(stderr,"0");
        byteBuffer[(i*8+bit)*4+3] = 0;
       } else {
        // fprintf(stderr,"1");
        byteBuffer[(i*8+bit)*4+3] = 255;
       }
     }
   }

   /* setup the Tk block structure */
   block.pixelPtr = byteBuffer;
   block.width = bmp.bmWidth;
   block.height = bmp.bmHeight;
   block.pitch = bmp.bmWidthBytes;
   block.pixelSize = bmp.bmBitsPixel/8;
   block.offset[0]  = offsetof(RGBQUAD,rgbRed);
   block.offset[1]  = offsetof(RGBQUAD,rgbGreen);
   block.offset[2]  = offsetof(RGBQUAD,rgbBlue);
   block.offset[3] = offsetof(RGBQUAD,rgbReserved);

   /* Create the image */
   result = Tcl_Eval(interp,"image create photo");
   if (result != TCL_OK) {
     return TCL_ERROR;
   }
   image_name = Tcl_GetStringResult(interp);
   photo = Tk_FindPhoto(interp, image_name);

   result = Tk_PhotoPutBlock( interp,photo, &block ,0,0,block.width, block.height,TK_PHOTO_COMPOSITE_SET);

   if (result != TCL_OK) {
     return TCL_ERROR;
   }

   //cleanup
   ckfree(bitBuffer);
   ckfree(byteBuffer);

   DeleteObject(iconInfo.hbmMask);
   DeleteObject(iconInfo.hbmColor);
   DestroyIcon(shfi.hIcon);

   Tcl_AppendResult(interp, image_name, NULL);
   return TCL_OK;
 }

 int DLLEXPORT
 Shellicon_Init(Tcl_Interp *interp)
 {
   if (Tcl_InitStubs(interp, "8.4", 0) == 0L) {
     return TCL_ERROR;
   }
   if (Tk_InitStubs(interp, "8.4", 0) == 0L) {
     return TCL_ERROR;
   }
   Tcl_CreateObjCommand(interp, "shellicon::get", GetIcon_Cmd, NULL, NULL);
   Tcl_PkgProvide(interp, "shellicon", "0.1");
   return TCL_OK;
 }


I wonder if there are a series of these types of commands which would be useful to put together in a distributable form, or, even, together submitted as a TIP to be included in the Windows Tcl distribution?

MJ - Extensions like these seem to be a prime candidate for a binary Tcllib/Tklib. Problem with adding this to the core is that similar functionallity for linux is almost impossible to build because it is so dependend on the file manager/destop being used.

LV I was just thinking about the fact that we already have speciality extensions like Registry in the Tcl core. I don't know that, specifically, this one is as important as the registry extension, but certainly I would think there is room for specialty extensions. There's several Windows special extensions like twapi.

NEM It should be possible to build a version of this for MacOS X. If I had the time, I'd do it myself.

FM There is some internationalization problems here due to encoding. I suppose it come from Tcl_TranslateFileName. I try myself to correct it but I really don't understand how Tcl works in matter of encoding. Note what the Tcl 8.5 manual says about this procedure Tcl_TranslateFileName:

However, with the advent of the newer Tcl_FSGetNormalizedPath and Tcl_GetNativePath, there is no longer any need to use this procedure. In particular, Tcl_GetNativePath performs all the necessary translation and encoding conversion, is virtual-filesystem aware, and caches the native result for faster repeated calls. Finally Tcl_GetNativePath does not require you to free anything afterwards.

MJ Please try updated version above. With the update, the following works:


% file mkdir \u0e01
% load shellicon0.1.dll
% label .l1 -image [::shellicon::get \u0e01]
% pack .l1