Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Writing a shell extension in plain C++

0.00/5 (No votes)
28 Sep 2013 1  
This article describe how to write a shell extension in plain C++ without ATL

Introduction

A shell extension is just a COM component with specific interfaces of shell. There are many articles on how to write shell extensions with ATL on codeproject. Though ATL is a really nice tool to build a COM object, we have to do it without ATL or MFC, just plain C++. In this way, we can understand how a Shell Extension works in more depth and even build it without commerical libraries from Microsoft.

In this article, the simple shell extension example The Complete Idiot's Guide to Writing Shell Extensions - Part I  will be rewritten without ATL. Its name is HelloExtNoATL.   

With ATL, most the processes are be done by project or class wizard. We don't need to write even one line to create the skeleton of Shell Extension project. Only methods for the COM object need be implemented by ourself. 

Without ATL, most the code for the COM object is the same, but IClassFactory and registration have to be done in our own code.  We can create it in follow steps:  

  1. Add a GUID
  2. Implement IClassFactory
  3. Implement CShellExt 
  4. Implement registration functions

Step 1: add a GUID for the COM object

Globally Universal Identifier (GUID) is mandatory for all COM objects. It's done by class wizard in Visual Studio for ATL. We can use guidgen.exe from Visual Studio or some online guid generator to create one for HelloExtNoATL without the help of Class Wizard.  

// 
// guid.h
//
// {CBF88FC2-F150-4F29-BC80-CE30EFD1B62C}
DEFINE_GUID(CLSID_HelloExtNoAtl, 0xcbf88fc2, 0xf150, 0x4f29, 0xbc, 0x80, 0xce, 0x30, 0xef, 0xd1, 0xb6, 0x2c);
 
  

Step 2: Implement IClassFactory   

It's one of major differences of COM objects between plain C++ and ATL. We have to implement our own ClassFactory. In ATL, it can be created automatically by an IDL file.

In CClassFactory, interface IUnknow and IClassFactory need be implemented. So the COM object interface methods can be queried and called. Its instance can be created with method CreateInstance and its ID.

 In constract, ATL has implemented IUnknown by default.  No more code for that. 

// 
// ClassFactory.h: interface for the CClassFactory class.
//

#if !defined(AFX_CLASSFACTORY_H__3B4B8B30_5B16_4B69_ACC8_25C7259A9F74__INCLUDED_)
#define AFX_CLASSFACTORY_H__3B4B8B30_5B16_4B69_ACC8_25C7259A9F74__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

extern HINSTANCE  g_hInst;
extern UINT       g_DllRefCount;

class CClassFactory : public IClassFactory  
{
protected:
	DWORD m_ObjRefCount;
public:
	CClassFactory();
	virtual ~CClassFactory();

   //IUnknown methods
   STDMETHODIMP QueryInterface(REFIID, LPVOID*);
   STDMETHODIMP_(DWORD) AddRef();
   STDMETHODIMP_(DWORD) Release();

   //IClassFactory methods
   STDMETHODIMP CreateInstance(LPUNKNOWN, REFIID, LPVOID*);
   STDMETHODIMP LockServer(BOOL) { return E_NOTIMPL; };
};

#endif // !defined(AFX_CLASSFACTORY_H__3B4B8B30_5B16_4B69_ACC8_25C7259A9F74__INCLUDED_) 

 
Most methods is plain and the same for all COM object classes. Method QueryInterface and  CreateInstance should be implemented accordingly for HelloExtNoATL.

In CClassFactory, this pointer is converted to IClassFactory * when the ID equals to IID_IClassFactory.

//
// CClassFactory::QueryInterface
//
STDMETHODIMP CClassFactory::QueryInterface( REFIID riid, LPVOID *ppReturn )
{
	*ppReturn = NULL;

	if( IsEqualIID(riid, IID_IUnknown) )
	    *ppReturn = this;
	else 
	    if( IsEqualIID(riid, IID_IClassFactory) )
		*ppReturn = (IClassFactory*)this;


	if( *ppReturn )
	{
		LPUNKNOWN pUnk = (LPUNKNOWN)(*ppReturn);
		pUnk->AddRef();
		return S_OK;
	}

	return E_NOINTERFACE;
} 

In CClassFactory::CreateInstance, the COM object CShellExt is created,  then its Interface pointer is returned by its ID,.
//
// CClassFactory::CreateInstance
//
STDMETHODIMP CClassFactory::CreateInstance( LPUNKNOWN pUnknown, REFIID riid, LPVOID *ppObject )
{
	*ppObject = NULL;
	if( pUnknown != NULL )
		return CLASS_E_NOAGGREGATION;

	// creates the namespace's main class
	CShellExt *pShellExt = new CShellExt();
	if( NULL==pShellExt ) 
		return E_OUTOFMEMORY;

	// query interface for the return value
	HRESULT hResult = pShellExt->QueryInterface(riid, ppObject);
	pShellExt->Release();
	return hResult;
}  

Step 3: Implement the COM class

HelloExtNoAtl just extends the context menu of Windows explorer. IShellExtInit and IContextMenu should be implemented in the COM class.  Since in pure plain C++, CShellExt inherits IShellExtInit and IContextMenu directly. Most code can be copied from the ATL sample directly.

 It also implements IUknown which is common for all COM objects.  In contract, ATL has implemented IUnknown by default. We have to code it ourself for class CShellExt. 

// 
// ShellExt.h: interface for the CShellExt class.
//

#if !defined(AFX_SHELLEXT_H__38EFB68B_5591_485A_9E50_409E8CDE10E2__INCLUDED_)
#define AFX_SHELLEXT_H__38EFB68B_5591_485A_9E50_409E8CDE10E2__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class CShellExt : public IShellExtInit, IContextMenu
{
protected:
	DWORD m_ObjRefCount;
public:
	CShellExt();
	virtual ~CShellExt();

	// IUnknown methods
	STDMETHOD (QueryInterface) (REFIID riid, LPVOID * ppvObj);
	STDMETHOD_ (ULONG, AddRef) (void);
	STDMETHOD_ (ULONG, Release) (void);
	

    // IShellExtInit
    STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

    // IContextMenu
    STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, LPSTR, UINT);
    STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
    STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
protected:
    TCHAR m_szFile [MAX_PATH];
};

#endif // !defined(AFX_SHELLEXT_H__38EFB68B_5591_485A_9E50_409E8CDE10E2__INCLUDED_)

Step 4: Registration

In ATL, a registration script is included in the project. It can be used to register and unregister a shell extension. But with plain C++, we have to implement all the registry operations manually.

Except adding registry entries for common COM objects, extra entries for shell extension also need be created. For HelloExtNoAtl, HKEY_CLASSES_ROOT\Folder\ShellEx\ContextMenuHandlers\HelloExtNoAtl is created. Because use it will extend the context menu functions of explorer. As the entry indicates, a menu entry will be appended when the context menu of a folder is popped up. 

// 
// HelloExtNoAtl.cpp: register and unregister
//
#include <windows.h>
#include <shlobj.h>

#include "HelloExtNoAtl.h"

#include "ClassFactory.h"
#include "Utility.h"

#include <olectl.h>

// data
HINSTANCE   g_hInst;
UINT        g_DllRefCount;

#pragma data_seg(".text")
#define INITGUID
#include <initguid.h>
#include <shlguid.h>
#include "Guid.h"
#pragma data_seg()

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
    switch (ul_reason_for_call)
	{
		case DLL_PROCESS_ATTACH:
			g_hInst = (HINSTANCE)hModule;
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
    }
    return TRUE;
}

/*---------------------------------------------------------------*/
// DllCanUnloadNow()
/*---------------------------------------------------------------*/
STDAPI DllCanUnloadNow( VOID )
{
	return (g_DllRefCount ? S_FALSE : S_OK);
}


/*---------------------------------------------------------------*/
// DllGetClassObject()
/*---------------------------------------------------------------*/
STDAPI DllGetClassObject( REFCLSID rclsid, REFIID riid, LPVOID *ppReturn )
{
	*ppReturn = NULL;

	// do we support the CLSID?
	if( !IsEqualCLSID(rclsid, CLSID_HelloExtNoAtl) )
	   return CLASS_E_CLASSNOTAVAILABLE;
   
	// call the factory
	CClassFactory *pClassFactory = new CClassFactory();
	if( pClassFactory==NULL )
	   return E_OUTOFMEMORY;
   
	// query interface for the a pointer
	HRESULT hResult = pClassFactory->QueryInterface(riid, ppReturn);
	pClassFactory->Release();
	return hResult;
}


/*---------------------------------------------------------------*/
// DllGetRegisterServer()
/*---------------------------------------------------------------*/

typedef struct{
   HKEY  hRootKey;
   LPTSTR lpszSubKey;
   LPTSTR lpszValueName;
   LPTSTR lpszData;
}REGSTRUCT, *LPREGSTRUCT;

STDAPI DllRegisterServer( VOID )
{
	INT i;
	HKEY hKey;
	LRESULT lResult;
	DWORD dwDisp;
	TCHAR szSubKey[MAX_PATH];
	TCHAR szCLSID[MAX_PATH];
	TCHAR szModule[MAX_PATH];
	LPWSTR pwsz;

	// get the CLSID in string form
	StringFromIID( CLSID_HelloExtNoAtl, &pwsz );

	if( pwsz )
	{
		WideCharToLocal( szCLSID, pwsz, ARRAYSIZE(szCLSID) );
        LPMALLOC pMalloc;
        CoGetMalloc(1, &pMalloc);
		if( pMalloc )
		{
	      pMalloc->Free(pwsz);
		  pMalloc->Release();
		}
    }

	// get this DLL's path and file name
	GetModuleFileName( g_hInst, szModule, ARRAYSIZE(szModule) );

	// CLSID entries
	REGSTRUCT ClsidEntries[] = {  
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  NULL,                   TEXT("HelloExtNoAtl"),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  TEXT("InfoTip"),        TEXT("HelloExtNoAtl."),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  NULL,                   TEXT("%s"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  TEXT("ThreadingModel"), TEXT("Apartment"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\DefaultIcon"),     NULL,                   TEXT("%s,0"),
        NULL,              NULL,                               NULL,                   NULL};

	for( i=0; ClsidEntries[i].hRootKey; i++ )
    {
		// create the sub key string.
		wsprintf( szSubKey, ClsidEntries[i].lpszSubKey, szCLSID );
        lResult = RegCreateKeyEx(  ClsidEntries[i].hRootKey,
                              szSubKey,
                              0,
                              NULL,
                              REG_OPTION_NON_VOLATILE,
                              KEY_WRITE,
                              NULL,
                              &hKey,
                              &dwDisp );
   
		if( lResult==NOERROR )
		{
			 TCHAR szData[MAX_PATH];
			 wsprintf(szData, ClsidEntries[i].lpszData, szModule);
			 lResult = RegSetValueEx( hKey,
                            ClsidEntries[i].lpszValueName,
                            0,
                            REG_SZ,
                            (LPBYTE)szData,
                            lstrlen(szData) + 1);
      
			 RegCloseKey(hKey);
		}
		else
			return SELFREG_E_CLASS;
    }

	// Context Menu
	lstrcpy( szSubKey, TEXT("Folder\\ShellEx\\ContextMenuHandlers\\HelloExtNoAtl"));
	lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT,
		szSubKey,
		0,
		NULL,
		REG_OPTION_NON_VOLATILE,
		KEY_WRITE,
		NULL,
		&hKey,
		&dwDisp);

	if( lResult==NOERROR )
	{
		TCHAR szData[MAX_PATH];
		lstrcpy(szData, szCLSID);
		lResult = RegSetValueEx( hKey,
			NULL,
			0,
			REG_SZ,
			(LPBYTE)szData,
			lstrlen(szData) + 1);

		RegCloseKey(hKey);
	}
	else
		return SELFREG_E_CLASS;

   // register the extension as approved by NT
   OSVERSIONINFO  osvi;
   osvi.dwOSVersionInfoSize = sizeof(osvi);
   GetVersionEx( &osvi );

   if( VER_PLATFORM_WIN32_NT == osvi.dwPlatformId )
   {
	   lstrcpy( szSubKey, TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"));
	   lResult = RegCreateKeyEx(  HKEY_LOCAL_MACHINE,
                              szSubKey,
                              0,
                              NULL,
                              REG_OPTION_NON_VOLATILE,
                              KEY_WRITE,
                              NULL,
                              &hKey,
                              &dwDisp);

       if( lResult==NOERROR )
       {
		   TCHAR szData[MAX_PATH];
	       lstrcpy(szData, TEXT("HelloExtNoAtl"));
           lResult = RegSetValueEx( hKey,
                                 szCLSID,
                                 0,
                                 REG_SZ,
                                 (LPBYTE)szData,
                                 lstrlen(szData) + 1);
      
	      RegCloseKey(hKey);
      }
	  else
		  return SELFREG_E_CLASS;
   }

   return S_OK;
}

/*---------------------------------------------------------------*/
// DllUnregisterServer()
/*---------------------------------------------------------------*/
STDAPI DllUnregisterServer( VOID )
{
	INT i;
	//HKEY hKey;
	LRESULT lResult;
	//DWORD dwDisp;
	TCHAR szSubKey[MAX_PATH];
	TCHAR szCLSID[MAX_PATH];
	TCHAR szModule[MAX_PATH];
	LPWSTR pwsz;

	// get the CLSID in string form
	StringFromIID( CLSID_HelloExtNoAtl, &pwsz );

	if( pwsz )
	{
		WideCharToLocal( szCLSID, pwsz, ARRAYSIZE(szCLSID) );
        LPMALLOC pMalloc;
        CoGetMalloc(1, &pMalloc);
		if( pMalloc )
		{
	      pMalloc->Free(pwsz);
		  pMalloc->Release();
		}
    }

	// get this DLL's path and file name
	GetModuleFileName( g_hInst, szModule, ARRAYSIZE(szModule) );

	// CLSID entries
	REGSTRUCT ClsidEntries[] = {  
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  NULL,                   TEXT("HelloExtNoAtl"),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s"),                  TEXT("InfoTip"),        TEXT("HelloExtNoAtl."),
		HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  NULL,                   TEXT("%s"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\InprocServer32"),  TEXT("ThreadingModel"), TEXT("Apartment"),
        HKEY_CLASSES_ROOT, TEXT("CLSID\\%s\\DefaultIcon"),     NULL,                   TEXT("%s,0"),
        NULL,              NULL,                               NULL,                   NULL};

	for( i=0; ClsidEntries[i].hRootKey; i++ )
        {
		// create the sub key string.
		wsprintf( szSubKey, ClsidEntries[i].lpszSubKey, szCLSID );
		lResult = RegDeleteKey(ClsidEntries[i].hRootKey, szSubKey);
        }


	// Context Menu
	lstrcpy( szSubKey, TEXT("Folder\\ShellEx\\ContextMenuHandlers\\HelloExtNoAtl"));
	lResult = RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);


   // register the extension as approved by NT
   OSVERSIONINFO  osvi;
   osvi.dwOSVersionInfoSize = sizeof(osvi);
   GetVersionEx( &osvi );

   if( VER_PLATFORM_WIN32_NT == osvi.dwPlatformId )
   {
	   lResult = RegDeleteKey(HKEY_LOCAL_MACHINE, szSubKey);
   }

	return S_OK;
}   

 

Installation

The installation step is the same for all COM objects whose standard register/unregister methods are implemented. Just use regsvr32.exe to install or uninstall it.  

What Does It All Look Like?  

 

 

 

Conclusion

After reading this article and trying the code, we can find the major difference of building a Shell Extension between ATL and plain C++. You can also read articles about creating COM objects in plain C or C++ on CodeProject.

 

References

COM-in-plain-C 

The Complete Idiot's Guide to Writing Shell Extensions - Part I

History

  October 28, 2013: Article first published.


License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here