The Code Project Click here for Dundas Software - 3D accelerated presentation quality ASP charting
Home >> Shell Programming >> Beginners

The Complete Idiot's Guide to Writing Shell Extensions - Part II
By Michael Dunn

A tutorial on writing a shell extension that operates on multiple files at once. 
 Beginner
 VC6, ATL, STL, Win9X/NT4/2000
 Posted 29 Mar 2000
 Updated 6 Jun 2000
Articles by this author
Send to a friend
Printer friendly version
Latest Articles Logon Message Boards Submit an Article
Broken links? Email us!
35 users have rated this article. result:
4.97 out of 5.

  • Download source files - 19 Kb
  • In Part I of the Guide, I gave an introduction to writing shell extensions, and demonstrated a simple context menu extension that operated on a single file at a time. In Part II, I'll demonstrate how to operate on multiple files in a single operation. The extension is a utility that can register and unregister COM servers. It also demonstrates how to use the ATL dialog class CDialogImpl. I will wrap up Part II by explaining some special registry keys that you can use to have your extension invoked on any file, not just preselected types.

    Part II assumes that you've read Part I so you know the basics of context menu extensions. You should also understand the basics of COM, ATL, and the STL collection classes.

    Beginning the context menu extension – what should it do?

    This shell extension will let you register and unregister COM servers in EXEs, DLLs, and OCXs. Unlike the extension we did in Part I, this extension will operate on all the files that are selected when the right-click event happens.

    Using AppWizard to get started

    Run the AppWizard and make a new ATL COM app. We'll call it DllReg. Keep all the default settings in the AppWizard, and click Finish. To add a COM object to the DLL, go to the ClassView tree, right-click the DllReg 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 DllRegShlExt in the Short Name edit box and click OK. (The other edit boxes on the panel will be filled in automatically.) This creates a class called CDllRegShlExt that contains the basic code for implementing a COM object. We will add our code to this class.

    The initialization interface

    Our IShellExtInit::Initialize() implementation will be quite different in this shell extension, for two reasons. First, we will enumerate all of the selected files. Second, we will test the selected files to see if they export registration and unregistration functions. We will consider only those files that export both DllRegisterServer() and DllUnregisterServer(). All other files will be ignored.

    We'll be using the list view control and the STL string and list classes, so you need to add these lines to the stdafx.h file:

    #include <commctrl.h>
    #include <string>
    #include <list>
    #include <atlwin.h>
    typedef std::list<std::basic_string<TCHAR> > string_list;

    Our CDllRegShlExt class will also need a few member variables:

    protected:
        HBITMAP     m_hRegBmp;
        HBITMAP     m_hUnregBmp;
        string_list m_lsFiles;
        TCHAR       m_szDir [MAX_PATH];

    The CDllRegShlExt constructor loads two bitmaps for use in the context menu:

    CDLLRegShlExt::CDLLRegShlExt()
    {
        m_hRegBmp = LoadBitmap ( _Module.GetModuleInstance(),
                                 MAKEINTRESOURCE(IDB_REGISTERBMP) );
    
        m_hUnregBmp = LoadBitmap ( _Module.GetModuleInstance(),
                                   MAKEINTRESOURCE(IDB_UNREGISTERBMP) );
    }

    After you add IShellExtInit to the list of interfaces implemented by CDllRegShlExt (see Part I for the instructions on doing this), we'll be ready to write the Initialize() function.

    Initialize() will perform these steps:

    1. Change the current directory to the directory being viewed in the Explorer window.
    2. Enumerate all of the files that were selected.
    3. For each file, try to load it with LoadLibrary().
    4. If LoadLibrary() succeeded, see if the file exports DllRegisterServer() and DllUnregisterServer().
    5. If both exports are found, add the filename to our list of files we can operate on, m_lsFiles.
    HRESULT CDllRegShlExt::Initialize ( 
        LPCITEMIDLIST pidlFolder,
        LPDATAOBJECT pDataObj,
        HKEY hProgID )
    {
    TCHAR     szFile    [MAX_PATH];
    TCHAR     szFolder  [MAX_PATH];
    TCHAR     szCurrDir [MAX_PATH];
    TCHAR*    pszLastBackslash;
    UINT      uNumFiles;
    HDROP     hdrop;
    FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HINSTANCE hinst;
    bool      bChangedDir = false;
    HRESULT (STDAPICALLTYPE* pfn)();

    Tons of boring local variables! The first step is to get an HDROP from the pDataObj passed in. This is done just like in the Part I extension.

        // Read the list of folders from the data object.  They're stored in HDROP
        // format, so just get the HDROP handle and then use the drag 'n' drop APIs
        // on it.
        if ( FAILED( pDO->GetData ( &etc, &stg )))
            return E_INVALIDARG;
    
        // Get an HDROP handle.
        hdrop = (HDROP) GlobalLock ( stg.hGlobal );
    
        if ( NULL == hdrop )
            {
            ReleaseStgMedium ( &stg );
            return E_INVALIDARG;
            }
    
        // Determine how many files are involved in this operation.
        uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );

    Next comes a for loop that gets the next filename (using DragQueryFile()) and tries to load it with LoadLibrary(). The real shell extension in the sample project does some directory-changing beforehand, which I have omitted here since it's a bit long.

        for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
            {
            // Get the next filename.
            if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ))
                continue;
    
            // Try & load the DLL.
            hinst = LoadLibrary ( szFile );
            
            if ( NULL == hinst )
                continue;

    Next, we'll see if the module exports the two required functions.

            // Get the address of DllRegisterServer();
            (FARPROC&) pfn = GetProcAddress ( hinst, "DllRegisterServer" );
    
            // If it wasn't found, skip the file.
            if ( NULL == pfn )
                {
                FreeLibrary ( hinst );
                continue;
                }
    
            // Get the address of DllUnregisterServer();
            (FARPROC&) pfn = GetProcAddress ( hinst, "DllUnregisterServer" );
    
            // If it was found, we can operate on the file, so add it to
            // our list of files (m_lsFiles).
            if ( NULL != pfn )
                {
                m_lsFiles.push_back ( szFile );
                }
    
            FreeLibrary ( hinst );
            }   // end for

    The last step (in the last if block) adds the filename to m_lsFiles, which is an STL list collection that holds strings. That list will be used later, when we iterate over all the files and register or unregister them.

    The last thing to do in Initialize() is free up resources and return the right value back to Explorer.

        // Release resources.
        GlobalUnlock ( stg.hGlobal );
        ReleaseStgMedium ( &stg );
    
        // If we found any files we can work with, return S_OK.  Otherwise,
        // return E_INVALIDARG so we don't get called again for this right-click
        // operation.
    
        return ( m_lsFiles.size() > 0 ) ? S_OK : E_INVALIDARG;
    }

    If you take a look at the sample project's code, you'll see that I have to figure out which directory is being viewed by looking at the names of the files. You might wonder why I don't just use the pidlFolder parameter, which is documented as "the item identifier list for the folder that contains the item whose context menu is being displayed." Well, during my testing on Windows 98, this parameter was always NULL, so it's useless.

    Adding our menu items

    Next up are the IContextMenu methods. As before, you'll need to add IContextMenu to the list of interfaces that CDllRegShlExt implements. And once again, the steps for doing this are in Part I of the Guide.

    We'll add two items to the menu, one to register the selected files, and another to unregister them. The items look like this:

     [context menu - 5K]

    Our QueryContextMenu() implementation starts out like in Part I. We check uFlags, and return immediately if the CMF_DEFAULTONLY flag is present.

    HRESULT CDLLRegShlExt::QueryContextMenu (
        HMENU hmenu,
        UINT  uMenuIndex,
        UINT  uidFirstCmd,
        UINT  uidLastCmd,
        UINT  uFlags )
    {
    UINT uCmdID = uidFirstCmd;
    
        // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
        if ( uFlags & CMF_DEFAULTONLY )
            {
            return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
            }

    Next up, we add the "Register servers" menu item. There's something new here: we set a bitmap for the item. This is the same thing that WinZip does to have the little folder-in-a-vice icon appear next to its own menu items.

        // Add our register/unregister items.
        InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++,
                     _T("Register server(s)") );
    
        // Set the bitmap for the register item.
        if ( NULL != m_hRegBmp )
            {
            SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, m_hRegBmp, NULL );
            }
    
        uMenuIndex++;

    The SetMenuItemBitmaps() API is how we show our little gears icon next to the "Register servers" item. Note that uCmdID is incremented, so that the next time we call InsertMenu(), the command ID will be one more than the previous value. At the end of this step, uMenuIndex is incremented so our second item will appear after the first one.

    And speaking of the second menu item, we add that next. It's almost identical to the code for the first item.

        InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++,
                     _T("Unregister server(s)") );
    
        // Set the bitmap for the unregister item.
        if ( NULL != m_hUnregBmp )
            {
            SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, m_hUnregBmp, NULL );
            }

    And at the end, we tell Explorer how many items we added.

        return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 2 );

    Providing fly-by help and a verb

    As before, the GetCommandString() method is called when Explorer needs to show fly-by help or get a verb for one of our commands. This extension is different than the last one in that we have 2 menu items, so we need to examine the uCmdID parameter to tell which item Explorer is calling us about.

    #include <atlconv.h>
    
    HRESULT CDLLRegShlExt::GetCommandString ( 
        UINT  uCmdID,
        UINT  uFlags, 
        UINT* puReserved,
        LPSTR szName,
        UINT  cchMax )
    {
    LPCTSTR szPrompt;
    
        USES_CONVERSION;
    
        if ( uFlags & GCS_HELPTEXT )
            {
            switch ( uCmdID )
                {
                case 0:
                    szPrompt = _T("Register all selected COM servers");
                break;
    
                case 1:
                    szPrompt = _T("Unregister all selected COM servers");
                break;
    
                default:
                    return E_INVALIDARG;
                break;
                }

    If uCmdID is 0, then we are being called for our first item (register). If it's 1, then we're being called for the second item (unregister). After we determine the help string, we copy it into the supplied buffer, converting to Unicode first if necessary.

            // Copy the help text into the supplied buffer.  If the shell wants
            // a Unicode string, we need to case szName to an LPCWSTR.
    
            if ( uFlags & GCS_UNICODE )
                {
                lstrcpynW ( (LPWSTR) szName, T2CW(szPrompt), cchMax );
                }
            else
                {
                lstrcpynA ( szName, T2CA(szPrompt), cchMax );
                }
            }

    For this extension, I also wrote code that provides a verb. However, when testing on Windows 98, Explorer never called GetCommandString() to get a verb. I even wrote a test app that called ShellExecute() on a DLL and tried to use a verb, but that didn't work either. I don't know if the behavior on NT is different. I have omitted the verb-related code here, but you can check it out in the sample project if you're interested.

    Carrying out the user's selection

    When the user clicks one of our menu items, Explorer calls our InvokeCommand() method. InvokeCommand() first checks the high word of lpVerb. If it's non-zero, then it is the name of the verb that was invoked. Since we know verbs aren't working properly (at least on Win 98), we'll bail out. Otherwise, if the low word of lpVerb is 0 or 1, we know one of our two menu items was clicked.

    HRESULT CDllRegShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
    {
        // If lpVerb really points to a string, ignore this function call and bail out.
        if ( 0 != HIWORD( pInfo->lpVerb ))
            return E_INVALIDARG;
    
        // Check that lpVerb is one of our commands (0 or 1)
        switch ( LOWORD( pInfo->lpVerb ))
            {
            case 0:
            case 1:
                {
                CProgressDlg dlg ( &m_lsFiles, pInfo );
    
                dlg.DoModal();
                return S_OK;
                }
            break;
    
            default:
                return E_INVALIDARG;
            break;
            }
    }

    If lpVerb is 0 or 1, we create a progress dialog (which is derived from the ATL class CDialogImpl), and pass it the list of filenames.

    All of the real work happens in the CProgressDlg class. Its OnInitDialog() function initializes the list control, and then calls CProgressDlg::DoWork(). DoWork() iterates over the string list that was built in CDllRegShlExt::Initialize(), and calls the appropriate function in each file. The basic code is below; it is not complete, since I've left out the error-checking code, and the parts that fill the list control. It's just enough to demonstrate how to iterate over the list of filenames and act on each one.

    void CProgressDlg::DoWork()
    {
    HRESULT (STDAPICALLTYPE* pfn)();
    string_list::const_iterator it, itEnd;
    HINSTANCE hinst;
    LPCSTR    pszFnName;
    HRESULT   hr;
    WORD      wCmd;
    
        wCmd = LOWORD ( m_pCmdInfo->lpVerb );
    
        // We only support 2 commands, so check the value passed in lpVerb.
        if ( wCmd > 1 )
            return;
    
        // Determine which function we'll be calling.  Note that these strings are
        // not enclosed in the _T macro, since GetProcAddress() only takes an
        // ANSI string for the function name.
        pszFnName = wCmd ? "DllUnregisterServer" : "DllRegisterServer";
    
        for ( it = m_pFileList->begin(), itEnd = m_pFileList->end();
              it != itEnd;
              it++ )
            {
            // Try to load the next file.
            hinst = LoadLibrary ( it->c_str() );
    
            if ( NULL == hinst )
                continue;
    
            // Get the address of the register/unregister function.
            (FARPROC&) pfn = GetProcAddress ( hinst, pszFnName );
    
            // If it wasn't found, go on to the next file.
            if ( NULL == pfn )
                continue;
    
            // Call the function!
            hr = pfn();

    I should explain that for loop, since the STL collection classes are a bit funky if you're not used to them. m_pFileList is a pointer to the m_lsFiles list in the CDllRegShlExt class. (This pointer was passed to the CProgressDlg constructor.) The STL list collection has a type called const_iterator, which is an abstract entity similar to MFC's POSITION type. A const_iterator variable acts like a pointer to a const object in the list, so the iterator can be dereferenced with -> to access the object itself. An iterator can also be incremented with ++ to walk forward in the list.

    So, the initialization expression of the for loop calls list::begin() to get an iterator that "points" at the first string in the list, and then calls list::end() to get an iterator that "points" at the "end" of the list, which is one position past the last string. (I put those terms in quotes to emphasize that the concepts of pointing, beginning, and end are all abstracted by the const_iterator type and must be accessed through const_iterator methods [like begin()] or operators [like ++].) Those iterators are assigned to it and itEnd, respectively. The loop continues while it does not equal itEnd; that is, while it has not yet reached the "end" of the list. The iterator it is incremented each time through the loop, so it walks the list one string at a time.

    The expression it->c_str() uses the -> operator on the iterator. Since it acts like a pointer to a string (remember, m_pFileList is a list of STL strings), it->c_str() calls the c_str() function in the string that it currently points to. c_str() returns a C-style character pointer, the equivalent of an LPCTSTR in this case.

    The remainder of DoWork() is cleanup and error handling. You can find the complete code in ProgressDlg.cpp in the sample project.

    (I just realized how strange it sounds to talk about a variable called "it". Sorry.) :)

    Registering the shell extension

    The DllReg extension operates on executable files, so let's register it to be invoked on EXE, DLL, and OCX files. As in Part I, we can do this through the RGS script, DllRegShlExt.rgs. Here's the necessary script to register our DLL as a context menu handler for each of those extensions.

    HKCR
    {
        NoRemove dllfile
        {
            NoRemove shellex
            {
                NoRemove ContextMenuHandlers
                {
                    ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
                }
            }
        }
        NoRemove exefile
        {
            NoRemove shellex
            {
                NoRemove ContextMenuHandlers
                {
                    ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
                }
            }
        }
        NoRemove ocxfile
        {
            NoRemove shellex
            {
                NoRemove ContextMenuHandlers
                {
                    ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
                }
            }
        }
    }

    The format of the RGS file, and the keywords NoRemove and ForceRemove are explained in Part I, in case you've forgotten their meaning.

    As in our previous extension, on NT/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. I won't show the code here, since it's just simple registry access, but you can find the code in this article's sample project.

    What does it all look like?

    When you click one of our menu items, the progress dialog is displayed and shows the results of the operations:

     [progress dialog - 21K]

    The list control shows the name of each file, and whether the function call succeeded or not. When you select a file, a message is shown beneath the list that gives more details, along with a description of the error if the function call failed.

    Other ways to register the extension

    So far, our extensions have been invoked only for certain file types. It's possible to have the shell call our extension for any file by registering as a context menu handler under the HKCR\* key:

    HKCR
    {
        NoRemove *
        {
            NoRemove shellex
            {
                NoRemove ContextMenuHandlers
                {
                    ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
                }
            }
        }
    }

    The HKCR\* key lists shell extensions that are called for all files. Note that the docs say that the extensions are also invoked for any shell object (meaning files, directories, virtual folders, Control Panel items, etc.), but that was not the behavior I saw during my testing. The extension was only invoked for files in the file system.

    In shell version 4.71+, there is also a key called HKCR\AllFileSystemObjects. If we register under this key, our extension is invoked for all files and directories in the file system, except root directories. (Extensions that are invoked for root directories are registered under HKCR\Drive.) However, I saw some strange behavior when registering under this key. The SendTo menu uses this key as well, and the DllReg menu items ended up being mixed in with the SendTo item:

     [context menu - 7K ]

    You can also write a context menu extension that operates on directories. For an example of such an extension, please refer to my article A Utility to Clean Up Compiler Temp Files.

    Finally, in shell version 4.71+, you can have a context menu extension invoked when the user right-clicks the background of an Explorer window that's viewing a directory (including the desktop). To have your extension invoked like this, register it under HKCR\Directory\Background\shellex\ContextMenuHandlers. Using this method, you can add your own menu items to the desktop context menu, or the menu for any other directory. The parameters passed to IShellExtInit::Initialize() are a bit different, though, so I may cover this topic in a future article.

    To be continued...

    Coming up in Part III, we'll examine a new type of extension, the QueryInfo handler, which displays pop-up descriptions of shell objects. I will also show how to use MFC in a shell extension.

    You can get the latest updates to this and my other articles at http://home.inreach.com/mdunn/code/

    About Michael Dunn

    Michael lives in sunny Los Angeles, California, and is so spoiled by the weather that he will probably never be able to live anywhere else. He graduated from UCLA with a math degree in 1995, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.

    He also enjoys his hobbies of playing pinball, bike riding, and the occasional PlayStation or Dreamcast game. (Game currently in the DC: Virtua Tennis.) He is also trying not to forget the languages he's studied: French, Mandarin Chinese, and Japanese.

    Click here to visit Michael Dunn's homepage.

    [Top] Rate this Article for us!     PoorExcellent  
    Hint: For improved responsiveness, use Internet Explorer 4 (or above) with Javascript enabled, choose 'Dynamic' from the View dropdown and hit 'Refresh'
     Keyword Filter
     View   Per page   Messages since
    New threadMessages 1 to 10 of 15 (Total: 15)First | Prev | Next | Last
    Subject 
    Author 
    Date 
      ATL7 - headers: error C2787
    JB 22:31 15 Mar 01 
      Re: ATL7 - headers: error C2787
    Michael Dunn 1:40 16 Mar 01 
      Re: ATL7 - headers: error C2787
    AdAstra 8:55 16 Mar 01 
      Only works with .dll files
    Bilby 9:21 6 Dec 00 
      Re: Only works with .dll files
    Michael Dunn 13:39 6 Dec 00 
      DragQueryFile with Shortcut Problem
    Brad Brilliant 9:53 1 Nov 00 
      Re: DragQueryFile with Shortcut Problem
    Michael Dunn 21:42 3 Nov 00 
      Are you familiar with AD UI extensions?
    Michal Arbel 8:52 2 Oct 00 
      Re: Are you familiar with AD UI extensions?
    Michael Dunn 21:39 3 Nov 00 
      Very impressive
    Kevin Cook 0:10 7 Jun 00 
    Last Visit: 12:00 Friday 1st January, 1999First | Prev | Next | Last

    Home >> Shell Programming >> Beginners
    last updated 6 Jun 2000
    Article content copyright Michael Dunn, 2000
    everthing else © CodeProject, 1999-2001.
    The Code Project Click here for Dundas Consulting - experts in MFC, C++, TCP/IP and ASP