The Complete Idiot's Guide to Writing Shell Extensions - Part III
By Maurice Montgenie (Original Version in ATL by Michael Dunn) 

A tutorial on writing a shell extension that shows pop-up info for files. 
 

  • Download demo project - 45 Kb

    In Parts I and II of the Guide, I showed how to write context menu extensions. In Part III, I'll demonstrate a different type of extension, explain how to share memory with the shell, and show how to use MFC alongside ATL.

    Part III assumes that you understand the basics of shell extensions (given in Part I), and are familiar with MFC. Note that the extension presented here requires version 4.71 or higher of the shell, so you must be running Windows 98 or 2000, or have the Active Desktop installed on 95 or NT 4.

    The QueryInfo extension

    The Active Desktop shell introduced a new feature, tooltips that show a description of certain objects if you hover the mouse over them. For example, hovering over My Computer shows this tooltip:

     [My Computer tooltip - 6K]

    Other objects like Network Neighborhood and Control Panel have similar tooltips. We can provide our own pop-up info for other objects in the shell, using a QueryInfo extension.

    A note about the name "QueryInfo extension": This is something I made up; I named it after one of the interfaces used, IQueryInfo. As far as I can tell, there isn't an official name for it. I took a quick look through the October 1999 MSDN and couldn't even find a mention of this type of extension! It is definitely a supported extension, though, since Microsoft Office installs QueryInfo extensions for its file types, as shown here:

     [Word doc tooltip - 5K]

    WinZip version 8 also has a QueryInfo extension for compressed files:

     [WinZip tooltip - 5K]

    The best documentation I've found is Dino Esposito's article "Enhance Your User's Experience with New Infotip and Icon Overlay Shell Extensions" in the March 2000 MSDN Magazine.

    Beginning the context menu extension – what should it do?

    This shell extension will be a quick text file viewer - it will display the first line of the file, along with the total file size. Our information will appear in a tooltip when the user hovers the mouse over a TXT file.

    Using AppWizard to get started

    Run the AppWizard and make a new ATL COM app. We'll call it TxtInfo. To add a COM object to the DLL, go to the ClassView tree, right-click the TxtInfo classes item, and pick New ATL Object.

    In the ATL Object Wizard, the first panel already has Simple Object selected, so just click Next. On the second panel, enter TxtInfoShlExt in the Short Name edit box and click OK. (The other edit boxes on the panel will be filled in automatically.)

    The initialization interface

    Before, with our context menu extensions, we implemented the IShellExtInit interface, which was how Explorer initialized our object. There is another initialization interface used for some shell extensions, IPersistFile, and this is the one a QueryInfo extension uses. Why the difference? If you remember, IShellExtInit::Initialize() receives an IDataObject pointer with which it can enumerate all of the files that were selected. Extensions that can only ever operate on a single file use IPersistFile. Since the mouse can't hover over more than one object at a time, a QueryInfo extension only works on one file at a time, so it uses IPersistFile.

    We first need to add IPersistFile to the list of interfaces that CTxtInfoShlExt implements. Open up TxtInfoShlExt.h, and add the lines listed here in red:

    asmTxtInfoIMap  InterfaceItem { pIID_ITxtInfoShlExt,  OFFSET vtableITxtInfoShlExt }
    InterfaceItem { pIID_IPersistFile, OFFSET vtableIPersistFile }
    END_INTERFACE_MAP
    We'll also need a variable to hold the filename that Explorer gives us during our initialization:
    asmTxtInfoObjData   STRUCT
    m_szFilename BYTE MAX_PATH DUP(?)
    asmTxtInfoObjData ENDS
    If you look up the docs on IPersistFile, you'll see a lot of methods. Fortunately, for the purposes of a shell extension, we only have to implement Load(), and ignore the others.

    Everything aside from Load() just returns E_NOTIMPL to indicate that we don't implement them.

    And to make this situation even nicer, our Load() method is really simple. We'll just store the name of the file that Explorer passes us. This is the file that the mouse is hovering over.

    Load proc this_:DWORD, pwszFilename:DWORD, dwMode:DWORD
    pObjectData this_, ecx ; cast this_ to object data
    lea ecx, (asmTxtInfoObjData ptr [ecx]).m_szFilename invoke WideCharToMultiByte, CP_ACP, 0, pwszFilename, -1, ecx, MAX_PATH, NULL, NULL mov eax, S_OK ret Load endp
    The filename is stored in m_szFilename, for later use.

    Creating text for the tooltip

    After Explorer calls our Load() method, it calls QueryInterface() to get another interface: IQueryInfo. IQueryInfo is a pretty simple interface, with just two methods (and only one is actually used). Open up TxtInfoShlExt.h again, and add the lines listed here in red:

    asmTxtInfoIMap  InterfaceItem { pIID_ITxtInfoShlExt,  OFFSET vtableITxtInfoShlExt }
    InterfaceItem { pIID_IPersistFile, OFFSET vtableIPersistFile }
    InterfaceItem { pIID_IQueryInfo, OFFSET vtableIQueryInfo }
    END_INTERFACE_MAP
    The GetInfoFlags() method isn't used currently, so we can just return E_NOTIMPL. GetInfoTip() is where we will return text to Explorer for it to show in the tooltip. First, the boring stuff at the beginning:
    GetInfoTip proc uses esi this_:DWORD, pdwFlags:DWORD, ppwszTip:DWORD   
    LOCAL pMalloc:DWORD
    LOCAL hFile:DWORD, hMapFile:DWORD, pMapViewFile:DWORD, dwFileSize:DWORD
    LOCAL bReadLine:DWORD
    LOCAL pszTooltip:DWORD
    LOCAL buffer[MAX_PATH]:BYTE
    LOCAL hResult:DWORD
    dwFlags is not used currently. ppwszTip is a pointer to an LPWSTR (Unicode string pointer) that we'll set to point at a buffer that we must allocate.

    First, we'll try to open the file for reading. We know the filename, since we stored it in the Load() function earlier.

      ;Opens a mapped view of the selected file
    ;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    invoke CreateFile, ecx, GENERIC_READ, 0, NULL, OPEN_EXISTING,\
    FILE_ATTRIBUTE_NORMAL, NULL
    mov hFile, eax .if eax == 0FFFFFFFFh mov hResult, E_FAIL jmp fin .endif invoke CreateFileMapping, hFile, NULL,PAGE_READONLY, 0, 0, NULL mov hMapFile, eax .if eax == 0 invoke CloseHandle,hFile mov hResult, E_FAIL jmp fin .endif invoke MapViewOfFile, hMapFile, FILE_MAP_READ, 0, 0, 0 mov pMapViewFile, eax .if eax == 0 invoke CloseHandle,hMapFile invoke CloseHandle,hFile mov hResult, E_FAIL jmp fin .endif
    Now, since we need to use the shell's memory allocator to allocate a buffer, we need an IMalloc interface pointer. We get this by calling the SHGetMalloc() function:
      invoke SHGetMalloc, addr pMalloc
    .if eax==E_FAIL
    jmp closefile
    .endif
    I'll have more to say about IMalloc a bit later. The next step is to get the file size, and read the first line:
      ;Get the size of the file
    invoke GetFileSize, hFile, NULL
    mov dwFileSize, eax
    ;Read in the first line from the file. ...
    The next step is to create the first part of the tooltip, which lists the file size.
      dsText szFileSize, "File size: "
    invoke lstrcat, pszTooltip, offset szFileSize
    invoke dwtoa, dwFileSize, addr buffer
    invoke lstrcat, pszTooltip, addr buffer
    .const
    szNewLine BYTE 13,10,0
    .code
    invoke lstrcat, pszTooltip, addr szNewLine

    Now, if we were able to read the first line of the file, we add it to the tooltip.

      pop ecx ;get the number of characters of the first line
    invoke lstrcpyn, addr buffer, pMapViewFile, ecx
    invoke lstrcat, pszTooltip, addr buffer
    Now that we have the complete tooltip, we need to allocate a buffer. Here's where we use IMalloc. The pointer returned by SHGetMalloc() is a copy of the shell's IMalloc interface. Any memory we allocate with that interface resides in the shell's process space, so the shell can use it. More importantly, the shell can also free it. So what we do is allocate the buffer, and then forget about it. The shell will free the memory once it's done using it.

    One other thing to be aware of is that the string we return to the shell must be in Unicode. That's why the calculation in the Alloc() call below multiplies by sizeof(wchar_t); just allocating memory for lstrlen(sToolTip) would only allocate half the required amount of memory.

      invoke lstrlen, pszTooltip
    inc eax
    push eax
    shl eax, 1
    coinvoke pMalloc, IMalloc, Alloc, eax
    mov ecx, ppwszTip
    mov [ecx], eax mov eax, ppwszTip
    .if eax==NULL
    mov hResult, E_OUTOFMEMORY
    jmp clearmem
    .endif ;Use the Unicode string copy function to put the tooltip text in the buffer.
    pop ecx
    mov edx, ppwszTip
    mov edx, [edx]
    invoke MultiByteToWideChar, CP_ACP, 0, pszTooltip, -1, edx, ecx
    The last thing to do is release the IMalloc interface we got earlier.
      coinvoke pMalloc, IMalloc, Release

    And that's all there is to it! Explorer takes the string in ppwszTip and displays it in a tooltip.

     [text file tooltip - 6K]

    Registering the shell extension

    QueryInfo extensions are registered a bit differently than context menu extensions. Our extension is registered under a subkey of HKEY_CLASSES_ROOT whose name is the file extension we want to handle. In this case, that's HKCR\.txt. But wait, it gets stranger! You'd think the ShellEx subkey would be something logical like "TooltipHandlers". Close! The key is called "{00021500-0000-0000-C000-000000000046}".

    I think Microsoft is trying to sneak some shell extensions past us here! If you poke around the registry, you'll find other ShellEx subkeys whose names are GUIDs. That GUID above happens to be the GUID of IQueryInfo.

    Anyway, here's the RGS script necessary to have our extension invoked on .TXT files:

    HKCR
    {
        NoRemove .txt
        {
            NoRemove shellex
            {
                NoRemove {00021500-0000-0000-C000-000000000046} = s '{F4D78AE1-05AB-11D4-8D3B-444553540000}'
            }
        }
    }

    You can easily have the extension invoked for other extensions by duplicating the above snippet, and changing ".txt" to whatever extension you want. Unfortunately, you can't register a QueryInfo extension under the * or AllFileSystemObjects keys to have it invoked on all files.

    As in our previous extensions, on NT and Win 2000 we need to add our extension to the list of "approved" extensions. The code to do this is in the DllRegisterServer() and DllUnregisterServer() functions in the sample project.

    The component in asm weigth only 15Ko, the same one coded with ATL is 28Ko !!!



  • [COMMENTS]

    Copyright © Maurice MONTGENIE - http://www.com4me.net