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:
- Add a GUID
- Implement IClassFactory
- Implement CShellExt
- 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.
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.
#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
extern HINSTANCE g_hInst;
extern UINT g_DllRefCount;
class CClassFactory : public IClassFactory
{
protected:
DWORD m_ObjRefCount;
public:
CClassFactory();
virtual ~CClassFactory();
STDMETHODIMP QueryInterface(REFIID, LPVOID*);
STDMETHODIMP_(DWORD) AddRef();
STDMETHODIMP_(DWORD) Release();
STDMETHODIMP CreateInstance(LPUNKNOWN, REFIID, LPVOID*);
STDMETHODIMP LockServer(BOOL) { return E_NOTIMPL; };
};
#endif
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.
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,.
STDMETHODIMP CClassFactory::CreateInstance( LPUNKNOWN pUnknown, REFIID riid, LPVOID *ppObject )
{
*ppObject = NULL;
if( pUnknown != NULL )
return CLASS_E_NOAGGREGATION;
CShellExt *pShellExt = new CShellExt();
if( NULL==pShellExt )
return E_OUTOFMEMORY;
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.
#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
class CShellExt : public IShellExtInit, IContextMenu
{
protected:
DWORD m_ObjRefCount;
public:
CShellExt();
virtual ~CShellExt();
STDMETHOD (QueryInterface) (REFIID riid, LPVOID * ppvObj);
STDMETHOD_ (ULONG, AddRef) (void);
STDMETHOD_ (ULONG, Release) (void);
STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
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
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.
#include <windows.h>
#include <shlobj.h>
#include "HelloExtNoAtl.h"
#include "ClassFactory.h"
#include "Utility.h"
#include <olectl.h>
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;
}
STDAPI DllCanUnloadNow( VOID )
{
return (g_DllRefCount ? S_FALSE : S_OK);
}
STDAPI DllGetClassObject( REFCLSID rclsid, REFIID riid, LPVOID *ppReturn )
{
*ppReturn = NULL;
if( !IsEqualCLSID(rclsid, CLSID_HelloExtNoAtl) )
return CLASS_E_CLASSNOTAVAILABLE;
CClassFactory *pClassFactory = new CClassFactory();
if( pClassFactory==NULL )
return E_OUTOFMEMORY;
HRESULT hResult = pClassFactory->QueryInterface(riid, ppReturn);
pClassFactory->Release();
return hResult;
}
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;
StringFromIID( CLSID_HelloExtNoAtl, &pwsz );
if( pwsz )
{
WideCharToLocal( szCLSID, pwsz, ARRAYSIZE(szCLSID) );
LPMALLOC pMalloc;
CoGetMalloc(1, &pMalloc);
if( pMalloc )
{
pMalloc->Free(pwsz);
pMalloc->Release();
}
}
GetModuleFileName( g_hInst, szModule, ARRAYSIZE(szModule) );
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++ )
{
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;
}
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;
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;
}
STDAPI DllUnregisterServer( VOID )
{
INT i;
LRESULT lResult;
TCHAR szSubKey[MAX_PATH];
TCHAR szCLSID[MAX_PATH];
TCHAR szModule[MAX_PATH];
LPWSTR pwsz;
StringFromIID( CLSID_HelloExtNoAtl, &pwsz );
if( pwsz )
{
WideCharToLocal( szCLSID, pwsz, ARRAYSIZE(szCLSID) );
LPMALLOC pMalloc;
CoGetMalloc(1, &pMalloc);
if( pMalloc )
{
pMalloc->Free(pwsz);
pMalloc->Release();
}
}
GetModuleFileName( g_hInst, szModule, ARRAYSIZE(szModule) );
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++ )
{
wsprintf( szSubKey, ClsidEntries[i].lpszSubKey, szCLSID );
lResult = RegDeleteKey(ClsidEntries[i].hRootKey, szSubKey);
}
lstrcpy( szSubKey, TEXT("Folder\\ShellEx\\ContextMenuHandlers\\HelloExtNoAtl"));
lResult = RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);
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.