[C / C++] Build a Windows Explorer Extention without ATL (part II)

Discussion in 'Mixed Languages' started by Michaela Joy, Mar 26, 2015.

  1. Michaela Joy

    Michaela Joy MDL Crazy Lady

    Jul 26, 2012
    4,071
    4,651
    150
    #1 Michaela Joy, Mar 26, 2015
    Last edited by a moderator: Apr 20, 2017
    Hi All,
    If you read my last post, you will notice that I left out an -extremely- important part of our COM control; The Class factory and the actual extension code itself. I wanted to dedicate an entire post to these topics because of their importance in the functionality of the COM control.

    So let's begin...

    Here's the definition of the Class Factory. Jeff Glatt takes all of the mystery out of COM by pointing out that a COM interface is nothing more than a struct with a virtual table of functions. If you don't know what a C struct is, I suggest you take a look at some of the basic courses on C programming. Basically, we will be using a C struct to emulate a C++ class. Tis is quite simple to do; a C++ class has a Vtable which is nothing more than a list of pointers to the various member functions. In our struct-with-Vtable, everything is public - other types of kind of scoping (as well as constructors / destructors) is done by the C++ compiler (We don't need to worry about that right now).

    Without further ado, here is ClassFactory.h

    Code:
    #ifndef CLASSFACTORY_H_INCLUDED
    #define CLASSFACTORY_H_INCLUDED
    
    HRESULT WINAPI CClassFactory_QueryInterface(IClassFactory *pThis,REFIID riid, LPVOID *ppvOut);
    ULONG WINAPI CClassFactory_AddRef(IClassFactory *pThis);
    ULONG WINAPI CClassFactory_Release(IClassFactory *pThis);
    HRESULT WINAPI CClassFactory_CreateInstance(IClassFactory *pThis, LPUNKNOWN pUnkOuter, REFIID riid, LPVOID *ppv);
    HRESULT WINAPI CClassFactory_LockServer(IClassFactory *pThis, BOOL fLock);
    
    IClassFactory *CClassFactory_Create(void);
    
    typedef struct _ClassFactoryStruct
    {
     IClassFactory fc;
     HINSTANCE m_hInstance;
     ULONG  m_ulRef;
    } ClassFactoryStruct;
    
    #endif // CLASSFACTORY_H_INCLUDED
    
    
    The first thing you'll notice is the 5 function prototypes, followed by the factory create subroutine. These 5 functions are called by Windows to work the Class Factory interface. QueryInterface() answers whether or not we are creating an interface that We support. AddRef() and Release() are called to add and remove references to our COM control. CreateInstance() is called to create an instance of our COM interface. Finally, LockServer() is called to lock and unlock the server. We don't use it. We handle our own reference counting. At some point, I'll experiment with it and post my findings.


    On to ClassFactory.c

    Code:
    #include <Windows.h>
    #include <shlobj.h>
    #include <stdio.h>
    #include "ClassFactory.h"
    #include "ContextMenu.h"
    
    IClassFactoryVtbl iclassFactoryVtbl = {
     CClassFactory_QueryInterface,
     CClassFactory_AddRef,
     CClassFactory_Release,
     CClassFactory_CreateInstance,
     CClassFactory_LockServer
    };
    
    
    What you see here is our implementation of the IClassFactory VTable. The order of these functions is important. Change it only if you want your code to crash.

    As Jeff Glatt mentioned, IClassFactoryVtbl is const, but you could build a dynamic table (Why...I don't know, but you may come up with some reason for doing it)

    The prototypes allow us to simply write the names and define the routines below. We plug the Vtable into our Class Factory struct when we create it.

    Code:
    
    IClassFactory *CClassFactory_Create(void)
    {
    ClassFactoryStruct *pCF = malloc(sizeof(ClassFactoryStruct));
    if(!pCF)
     return NULL;
    pCF->fc.lpVtbl = &iclassFactoryVtbl;
    pCF->m_hInstance  = g_hInst;
    pCF->m_ulRef = 1;
    ++g_uiRefThisDll;
    return &pCF->fc;
    }
    
    Easy - Peasy. :)

    QueryInterface is just as simple. We check for IID_IClassFactory. We make sure that this interface is not IID_IUnknown. These are GUIDs that are declared in Windows. If a ClassFactory is needed, we increment our reference counter and return NOERROR, making sure to set the pointer return value *ppv;

    Code:
    
    HRESULT WINAPI CClassFactory_QueryInterface(IClassFactory *pThis, REFIID riid, LPVOID *ppv)
    {
    ClassFactoryStruct *pCF = (ClassFactoryStruct *)pThis;
    
    if (IsEqualIID(riid,&IID_IClassFactory) && !IsEqualIID(riid,&IID_IUnknown))
    {
     *ppv = pThis;
     ++pCF->m_ulRef;
     return NOERROR;
    }
    else
    {
     *ppv = NULL;
     return E_NOINTERFACE;
    }
    }
    
    
    AddRef() and Release() handle reference counting, making sure to destroy the instance when we're done with it.

    Code:
    
    ULONG WINAPI CClassFactory_AddRef(IClassFactory *pThis)
    {
    ClassFactoryStruct *pCF = (ClassFactoryStruct *)pThis;
    return ++pCF->m_ulRef;
    }
    
    ULONG WINAPI CClassFactory_Release(IClassFactory *pThis)
    {
    ClassFactoryStruct *pCF = (ClassFactoryStruct*)pThis;
    if (--pCF->m_ulRef == 0)
    {
     free(pThis);
     --g_uiRefThisDll;
     return 0;
    }
    return pCF->m_ulRef;
    }
    

    CreateInstance() creates an instance of our Class factory (As its' name implies) We don't need to handle aggregation in our code, so we return if the outer COM object is not null. If all goes well, we simply create the Context Menu COM Object.
    The calls to QueryInterface() and Release() Set up the Extension.

    Code:
    
    HRESULT WINAPI CClassFactory_CreateInstance(IClassFactory *pThis,LPUNKNOWN pUnkOuter,REFIID riid,LPVOID *ppv)
    {
    *ppv = NULL;
    if (pUnkOuter)
     return CLASS_E_NOAGGREGATION;
    HRESULT hr = S_OK;
    IContextMenu *pIContextMenu = CContextMenuExt_Create();
    if (pIContextMenu == NULL)
     return E_OUTOFMEMORY;
    hr = pIContextMenu->lpVtbl->QueryInterface(pIContextMenu,riid,ppv);
    pIContextMenu->lpVtbl->Release(pIContextMenu);
    return hr;
    }
    
    And of course, LockServer() Not implemented.

    Code:
    HRESULT WINAPI CClassFactory_LockServer(IClassFactory *pThis,BOOL fLock)
    {
    return E_NOTIMPL;
    }
    
    
    Okay. So here's ContextMenu.h

    As you probably noticed, We use the same basic structure as we did for the Class Factory, with the exception of having two interfaces in one structure. We do have some new functions, but the "big 3" are always there (QueryInterface(), AddRef(), and release() )

    Our new routines for the IContextMenuExt interface are QueryContextMenu(),
    InvokeCommand(), and GetCommandString().

    For IShellInitExt, Initialize() is the latest addition. We'll add code to grab selected files and other various stuff. More on this later.

    Code:
    #ifndef CONTEXTMENU_H_INCLUDED
    #define CONTEXTMENU_H_INCLUDED
    
    HRESULT WINAPI CContextMenuExt_QueryInterface(IContextMenu *pThis,REFIID riid,LPVOID *ppvOut);
    ULONG WINAPI CContextMenuExt_AddRef(IContextMenu *pThis);
    ULONG WINAPI CContextMenuExt_Release(IContextMenu *pThis);
    HRESULT WINAPI CContextMenuExt_QueryContextMenu(IContextMenu *pThis,HMENU hMenu,UINT uiIndexMenu,UINT idCmdFirst,UINT idCmdLast,UINT uFlags);
    HRESULT WINAPI CContextMenuExt_InvokeCommand(IContextMenu *pThis, LPCMINVOKECOMMANDINFO lpici);
    HRESULT WINAPI CContextMenuExt_GetCommandString(IContextMenu *pThis,UINT_PTR idCmd,UINT uFlags,UINT *pwReserved,LPSTR pszName,UINT cchMax);
    
    IContextMenu *CContextMenuExt_Create(void);
    
    typedef struct _ContextMenuExtStruct
    {
     IContextMenu   cm;
     IShellExtInit  si;
     ULONG m_ulRef;
    } ContextMenuExtStruct;
    
    HRESULT WINAPI CShellInitExt_QueryInterface(IShellExtInit *pThis, REFIID riid, LPVOID* ppvObject);
    ULONG WINAPI CShellInitExt_AddRef(IShellExtInit *pThis);
    ULONG WINAPI CShellInitExt_Release(IShellExtInit *pThis);
    HRESULT WINAPI CShellInitExt_Initialize(IShellExtInit *pThis,LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj, HKEY hKeyProgID);
    
    extern UINT g_uiRefThisDll;
    extern HINSTANCE g_hInst;
    
    #endif // CONTEXTMENU_H_INCLUDED
    
    
    Now, let's take a look at ContextMenu.c. Basically, the same pattern is applied here. Declare the VTables and attach them to the Interfaces that we create.

    Code:
    #include <Windows.h>
    #include <shlobj.h>
    #include <wchar.h>
    #include <tchar.h>
    #include "ContextMenu.h"
    #include "resource.h"
    
    IContextMenuVtbl icontextMenuVtbl = {
     CContextMenuExt_QueryInterface,
     CContextMenuExt_AddRef,
     CContextMenuExt_Release,
     CContextMenuExt_QueryContextMenu,
     CContextMenuExt_InvokeCommand,
     CContextMenuExt_GetCommandString
    };
    
    IShellExtInitVtbl ishellInitExtVtbl = {
        CShellInitExt_QueryInterface,
        CShellInitExt_AddRef,
        CShellInitExt_Release,
        CShellInitExt_Initialize
    };
    
    IContextMenu *CContextMenuExt_Create(void)
    {
    ContextMenuExtStruct *pCM = malloc(sizeof(ContextMenuExtStruct));
    if(!pCM)
     return NULL;
    pCM->cm.lpVtbl = &icontextMenuVtbl;
    pCM->si.lpVtbl = &ishellInitExtVtbl;
    pCM->m_ulRef = 1;
    g_uiRefThisDll++;
    return &pCM->cm;
    }
    
    

    And of course, QueryInterface() is similar to the version called in ClassFactory.c.

    Code:
    
    HRESULT WINAPI CContextMenuExt_QueryInterface(IContextMenu *pThis, REFIID riid, LPVOID *ppv)
    {
    ContextMenuExtStruct *pCM = (ContextMenuExtStruct *)pThis;
    if (IsEqualIID(riid,&IID_IUnknown) || IsEqualIID (riid,&IID_IContextMenu))
    {
     *ppv = pThis;
     pCM->m_ulRef++;
     return S_OK;
    }
    else if (IsEqualIID(riid,&IID_IShellExtInit))
    {
     *ppv = &pCM->si;
     pCM->m_ulRef++;
     return S_OK;
    }
    else
    {
     *ppv = NULL;
     return E_NOINTERFACE;
    }
    }
    
    ULONG WINAPI CContextMenuExt_AddRef(IContextMenu *pThis)
    {
    ContextMenuExtStruct *pCM = (ContextMenuExtStruct *)pThis;
    return ++pCM->m_ulRef;
    }
    
    
    ULONG WINAPI CContextMenuExt_Release(IContextMenu *pThis)
    {
    ContextMenuExtStruct *pCM = (ContextMenuExtStruct *)pThis;
    if (--pCM->m_ulRef == 0)
    {
     free(pThis);
     g_uiRefThisDll--;
     return 0;
    }
    return pCM->m_ulRef;
    }
    
    
    This routine is used to fill in the Context Menu entries. You can do a lot of neat things here. Shown is a simple way to add a bitmap to the menu item. If you do this, you'll need to declare the ID for the bitmap and include a Resource script file with your project. Typically, it's a 22x22, 256 color bitmap.

    Code:
    HRESULT WINAPI CContextMenuExt_QueryContextMenu(IContextMenu *pThis, HMENU hMenu, UINT uiIndexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
    {
    HBITMAP hBitmap;
    if (uFlags & CMF_DEFAULTONLY)
     return MAKE_HRESULT(SEVERITY_SUCCESS,FACILITY_NULL,0);
    InsertMenu(hMenu,0, MF_SEPARATOR | MF_BYPOSITION, 0, NULL);
    InsertMenu(hMenu,1, MF_STRING | MF_BYPOSITION, (idCmdFirst + ID_SHELLTEST_C), _T("Shell Test Item..."));
    InsertMenu(hMenu,2, MF_SEPARATOR | MF_BYPOSITION, 0, NULL);
    hBitmap = (HBITMAP)LoadBitmap(g_hInst,MAKEINTRESOURCE(IDI_MAINMENU));
    SetMenuItemBitmaps(hMenu,1,MF_BITMAP | MF_BYPOSITION,hBitmap,hBitmap);
    return MAKE_HRESULT(0, FACILITY_NULL, ID_SHELLTEST_C + 1);
    }
    
    InvokeCommand() gets called to process the menu choice. The IDs are defined in resource.h

    Code:
    HRESULT WINAPI CContextMenuExt_InvokeCommand(IContextMenu *pThis, LPCMINVOKECOMMANDINFO lpici)
    {
    //ContextMenuExtStruct *pCM = (ContextMenuExtStruct *)pThis;
    switch(LOWORD(lpici->lpVerb))
    {
     case ID_SHELLTEST_C:
       MessageBox(NULL, "Command Invoked", NULL, MB_OK + MB_ICONINFORMATION);
     return S_OK;
     default:
     return E_FAIL;
    }
    }
    
    GetCommandString() is not implemented.

    Code:
    
    HRESULT WINAPI CContextMenuExt_GetCommandString(IContextMenu *pThis, UINT_PTR idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax)
    {
    return E_NOTIMPL;
    }
    
    This is the implementation of the IShellInitExt interface.

    Code:
    HRESULT WINAPI CShellInitExt_QueryInterface(IShellExtInit *pThis, REFIID riid, LPVOID *ppv)
    {
    return ((IContextMenu *)(pThis - 1))->lpVtbl->QueryInterface((IContextMenu *)(pThis - 1),riid,ppv);
    }
    
    ULONG WINAPI CShellInitExt_AddRef(IShellExtInit *pThis)
    {
    return ((IContextMenu *)(pThis - 1))->lpVtbl->AddRef((IContextMenu *)(pThis - 1));
    }
    
    ULONG WINAPI CShellInitExt_Release(IShellExtInit *pThis)
    {
    return ((IContextMenu *)(pThis - 1))->lpVtbl->Release((IContextMenu *)(pThis - 1));
    }
    
    We process Initialize() to deal with such things as drag and drop or selections in the Explorer.
    Code:
    HRESULT WINAPI CShellInitExt_Initialize(IShellExtInit *pThis, LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj, HKEY hKeyProgID)
    {
    FORMATETC   fe;
    STGMEDIUM   stgmed;
    
    fe.cfFormat   = CF_HDROP;
    fe.ptd        = NULL;
    fe.dwAspect   = DVASPECT_CONTENT;
    fe.lindex     = -1;
    fe.tymed      = TYMED_HGLOBAL;
    if (lpdobj != NULL)
    {
     HRESULT hr = lpdobj->lpVtbl->GetData(lpdobj,&fe,&stgmed);
     if (SUCCEEDED(hr))
     {
      if(stgmed.hGlobal)
      {
    //  LPDROPFILES pDropFiles = (LPDROPFILES)GlobalLock(stgmed.hGlobal);
    //  GlobalUnlock(stgmed.hGlobal);
      }
      ReleaseStgMedium(&stgmed);
     }
     else
      return E_UNEXPECTED;
    }
    return S_OK;
    }
     
    
    For completeness, here's resource.h
    Code:
    #ifndef RESOURCE_H_INCLUDED
    #define RESOURCE_H_INCLUDED
    #define IDI_MAINMENU 1001
    #define ID_SHELLTEST_C 1
    
    #endif // RESOURCE_H_INCLUDED
    
    
    Followed by ShellTest_C.rc

    Code:
    #include <windows.h>
    #include <commctrl.h>
    #include <richedit.h>
    #include "resource.h"
    LANGUAGE LANG_ENGLISH,SUBLANG_ENGLISH_US
    VS_VERSION_INFO VERSIONINFO
    FILEVERSION 1,0,0,0
    PRODUCTVERSION 1,0,0,0
    FILEFLAGSMASK 0x3F
    FILEFLAGS 0x0
    FILEOS VOS_NT_WINDOWS32
    FILETYPE VFT_APP
    FILESUBTYPE VFT2_UNKNOWN
    {
      BLOCK "StringFileInfo"
      {
        BLOCK "040904B0"
        {
          VALUE "CompanyName", "MJ and Company\0"
          VALUE "FileDescription", "Shell Extension Test\0"
          VALUE "FileVersion", "1.00\0"
          VALUE "InternalName", "ShellTest_C\0"
          VALUE "LegalCopyright", "MJ © 2015\0"
          VALUE "OriginalFilename", "ShellTest_C.dll\0"
          VALUE "ProductName", "ShellTest_C.dll\0"
          VALUE "ProductVersion", "1.00\0"
        }
      }
      BLOCK "VarFileInfo"
      {
        VALUE "Translation", 0x409, 0x4B0
      }
    }
    
    IDI_MAINMENU BITMAP "ShellTest_C_MainMenu.bmp"
     
    
    In my next post, I'll go over setting up TDM-GCC with Code::Blocks to build this project.

    :MJ
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...