用纯 C++ 编写外壳扩展
本文描述了如何在没有 ATL 的情况下用纯 C++ 编写外壳扩展
引言
Shell 扩展只是具有特定 shell 接口的 COM 组件。CodeProject 上有很多关于如何使用 ATL 编写 shell 扩展的文章。虽然 ATL 是构建 COM 对象的绝佳工具,但我们必须在没有 ATL 或 MFC 的情况下,仅使用纯 C++ 来完成。这样,我们可以更深入地了解 Shell 扩展的工作原理,甚至可以在没有 Microsoft 商业库的情况下构建它。
在本文中,简单的 shell 扩展示例 编写 Shell 扩展的完整白痴指南 - 第一部分 将在没有 ATL 的情况下重写。它的名称是 HelloExtNoATL。
使用 ATL,大多数流程都由项目或类向导完成。我们甚至不需要编写一行代码来创建 Shell 扩展项目的骨架。只有 COM 对象的方法需要我们自己实现。
如果没有 ATL,COM 对象的大部分代码都是相同的,但是 IClassFactory 和注册必须在我们自己的代码中完成。 我们可以按以下步骤创建它:
- 添加 GUID
- 实现 IClassFactory
- 实现 CShellExt
- 实现注册函数
步骤 1:为 COM 对象添加 GUID
全局唯一标识符 (GUID) 对于所有 COM 对象都是强制性的。在 Visual Studio 中,ATL 通过类向导完成。我们可以使用 Visual Studio 中的 guidgen.exe 或一些在线 guid 生成器来为 HelloExtNoATL 创建一个 GUID,而无需类向导的帮助。
//
// guid.h
//
// {CBF88FC2-F150-4F29-BC80-CE30EFD1B62C}
DEFINE_GUID(CLSID_HelloExtNoAtl, 0xcbf88fc2, 0xf150, 0x4f29, 0xbc, 0x80, 0xce, 0x30, 0xef, 0xd1, 0xb6, 0x2c);
步骤 2:实现 IClassFactory
这是纯 C++ 和 ATL 之间 COM 对象的主要区别之一。我们必须实现我们自己的 ClassFactory。在 ATL 中,它可以通过 IDL 文件自动创建。
在 CClassFactory 中,需要实现接口 IUnknow 和 IClassFactory。因此,可以查询和调用 COM 对象接口方法。可以使用方法 CreateInstance 及其 ID 创建其实例。
相反,ATL 默认实现了 IUnknown。 不需要更多代码。
//
// 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_)
大多数方法都是通用的,并且对于所有 COM 对象类都相同。应该为 HelloExtNoATL 相应地实现方法 QueryInterface 和 CreateInstance。
在 CClassFactory 中,当 ID 等于 IID_IClassFactory 时,this 指针将转换为 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;
}
在 CClassFactory::CreateInstance 中,创建 COM 对象 CShellExt, 然后其接口指针通过其 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;
}
步骤 3:实现 COM 类
HelloExtNoAtl 只是扩展了 Windows 资源管理器的上下文菜单。IShellExtInit 和 IContextMenu 应在 COM 类中实现。 由于在纯 C++ 中,CShellExt 直接继承 IShellExtInit 和 IContextMenu。大多数代码可以直接从 ATL 示例中复制。
它还实现了 IUknown,这对于所有 COM 对象都是常见的。 相反,ATL 默认实现了 IUnknown。我们必须为类 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_)
步骤 4:注册
在 ATL 中,注册脚本包含在项目中。它可以用于注册和取消注册 shell 扩展。但是,使用纯 C++,我们必须手动实现所有注册表操作。
除了为常见 COM 对象添加注册表条目外,还需要为 shell 扩展创建额外的条目。对于 HelloExtNoAtl,创建了 HKEY_CLASSES_ROOT\Folder\ShellEx\ContextMenuHandlers\HelloExtNoAtl。因为使用它将扩展资源管理器的上下文菜单功能。正如该条目所示,当弹出文件夹的上下文菜单时,将附加一个菜单条目。
//
// 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;
}
安装
对于所有实现了标准注册/取消注册方法的 COM 对象,安装步骤都是相同的。只需使用 regsvr32.exe 安装或卸载它。
这一切看起来像什么?

结论
在阅读本文并尝试该代码后,我们可以发现使用 ATL 和纯 C++ 构建 Shell 扩展的主要区别。您还可以阅读 CodeProject 上有关使用纯 C 或 C++ 创建 COM 对象的文章。
参考文献
编写外壳扩展的 Complete Idiot's Guide -
历史
2013 年 10 月 28 日:文章首次发布。