编写 Shell 扩展的完整入门指南 - 第二部分






4.97/5 (44投票s)
2000 年 3 月 29 日

635984

6309
一个关于编写一次操作多个文件的 shell 扩展的教程。
目录
引言
在《指南》的第一部分中,我介绍了编写 shell 扩展,并演示了一个一次操作单个文件的简单上下文菜单扩展。在第二部分,我将展示如何在一次右键单击操作中处理多个文件。本文的示例扩展是一个可以注册和注销 COM 服务器的实用程序。它还演示了如何使用 ATL 对话框类CDialogImpl
。我将在第二部分最后解释一些特殊的注册表键,您可以使用它们来让您的扩展在任何文件上被调用,而不仅仅是预选类型的文件。
第二部分假定您已阅读第一部分,因此您了解上下文菜单扩展的基础知识。您还应该了解 COM、ATL 和 STL 集合类的基础知识。
请记住,VC 7(可能还有 VC 8)用户在编译之前需要更改一些设置。有关详细信息,请参阅第一部分中自述文件部分。
使用 AppWizard 开始
运行 AppWizard,创建一个新的 ATL COM 程序。我们将其命名为“DllReg”。在 AppWizard 中保留所有默认设置,然后单击完成。在 VC 7 中,请务必取消选中属性复选框;在此示例中,我们不会使用属性 ATL。要将 COM 对象添加到 DLL 中,请转到 ClassView 树,右键单击DllReg classes项,然后选择新建 ATL 对象。(在 VC 7 中,右键单击该项并选择添加|添加类。)
在 ATL 对象向导中,第一个面板已选中简单对象,因此只需单击下一步。在第二个面板上,在短名称编辑框中输入“DllRegShlExt”(面板上的其他编辑框将自动填充)。
默认情况下,向导创建一个 COM 对象,该对象可以通过 OLE 自动化从 C 和脚本类客户端使用。我们的扩展将仅由 Explorer 使用,因此我们可以更改一些设置以删除自动化功能。转到属性页面,并将接口类型更改为自定义,并将聚合设置更改为否。
当您单击确定时,向导将创建一个名为CDLLRegShlExt
的类,其中包含实现 COM 对象的基本代码。我们将把我们的代码添加到此类中。
我们将使用列表视图控件以及 STL 的string
和list
类,因此请在现有#include
行(包含 ATL 头文件)之后,将这些行添加到stdafx.h
中。
#include <atlwin.h> #include <commctrl.h> #include <string> #include <list> typedef std::list< std::basic_string<TCHAR> > string_list;
初始化接口
我们的IShellExtInit::Initialize()
实现将与第一部分的扩展大不相同,原因有两个。首先,我们将枚举所有选定的文件。其次,我们将测试选定的文件,以查看它们是否导出了注册和注销函数。我们将只考虑导出DllRegisterServer()
和DllUnregisterServer()
的所有文件。所有其他文件都将被忽略。
我们像在第一部分中一样开始,移除一些向导生成的代码,并将IShellExtInit
接口添加到 C++ 类中。
#include <shlobj.h> #include <comdef.h> class ATL_NO_VTABLE CDLLRegShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CDLLRegShlExt, &CLSID_DllRegShlExt>,public IDllRegShlExt,public IShellExtInit { BEGIN_COM_MAP(CDLLRegShlExt)COM_INTERFACE_ENTRY(IDllRegShlExt)COM_INTERFACE_ENTRY(IShellExtInit) END_COM_MAP()
我们的CDLLRegShlExt
类还需要一些成员变量。
protected:
HBITMAP m_hRegBmp;
HBITMAP m_hUnregBmp;
string_list m_lsFiles;
CDLLRegShlExt
构造函数加载两个位图以在上下文菜单中使用。
CDLLRegShlExt::CDLLRegShlExt() { m_hRegBmp = LoadBitmap ( _Module.GetModuleInstance(), MAKEINTRESOURCE(IDB_REGISTERBMP) ); m_hUnregBmp = LoadBitmap ( _Module.GetModuleInstance(), MAKEINTRESOURCE(IDB_UNREGISTERBMP) ); }
现在我们可以编写Initialize()
函数了。Initialize()
将执行以下步骤:
- 将当前目录更改为 Explorer 窗口中正在查看的目录。
- 枚举所有选定的文件。
- 对于每个 DLL 和 OCX 文件,尝试使用
LoadLibrary()
加载它。 - 如果
LoadLibrary()
成功,请查看文件是否导出了DllRegisterServer()
和DllUnregisterServer()
。 - 如果找到两个导出,请将文件名添加到我们可以操作的文件列表中,即
m_lsFiles
。
HRESULT CDLLRegShlExt::Initialize (
LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID )
{
UINT uNumFiles;
HDROP hdrop;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT,
-1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HINSTANCE hinst;
HRESULT (STDAPICALLTYPE* pfn)();
大量无聊的局部变量!第一步是从传入的pDataObj
获取一个HDROP
。这与第一部分的扩展相同。
// 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 );
接下来是一个 for 循环,它获取下一个文件名(使用DragQueryFile()
)并尝试使用LoadLibrary()
加载它。实际的 shell 扩展在示例项目中会提前进行一些目录更改,我在这里省略了,因为它有点长。
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;
接下来,我们将查看模块是否导出了两个必需的函数。
// 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
如果模块中存在两个导出的函数,则将文件名添加到m_lsFiles
中,这是一个 STL list
集合,用于存储字符串。该列表稍后将用于迭代所有文件并注册或注销它们。
在Initialize()
中要做的最后一件事是释放资源并将正确的值返回给 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; }
如果您查看示例项目的代码,您会发现我必须通过查看文件名来确定正在查看哪个目录。您可能会想,为什么我不直接使用pidlFolder
参数,该参数被记录为“包含上下文菜单正在显示的项的文件夹的项标识符列表”。好吧,在我进行测试期间,此参数始终为NULL
,因此它毫无用处。
添加我们的菜单项
接下来是IContextMenu
方法。与以前一样,我们将IContextMenu
添加到CDLLRegShlExt
实现的接口列表中,通过添加粗体行:
class ATL_NO_VTABLE CDLLRegShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CDLLRegShlExt, &CLSID_DllRegShlExt>, public IShellExtInit, public IContextMenu { BEGIN_COM_MAP(CDLLRegShlExt) COM_INTERFACE_ENTRY(IShellExtInit) COM_INTERFACE_ENTRY(IContextMenu) END_COM_MAP()
我们将向菜单添加两个项,一个用于注册选定的文件,另一个用于注销它们。这些项如下所示:
我们的QueryContextMenu()
实现开始时与第一部分相同。我们检查uFlags
,如果存在CMF_DEFAULTONLY
标志,则立即返回。
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);
接下来,我们添加“注册服务器”菜单项。这里有一个新功能:我们为该项设置了一个位图。这与 WinZip 为其自己的菜单项显示小齿轮图标所做的事情相同。
// 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++;
SetMenuItemBitmaps()
API 是我们显示小齿轮图标并使其显示在“注册服务器”项旁边的方法。请注意,uCmdID
已递增,因此下次调用InsertMenu()
时,命令 ID 将比前一个值大一。在此步骤结束时,uMenuIndex
已递增,以便我们的第二个项显示在第一个项之后。
说到第二个菜单项,我们接下来添加它。它与第一个项的代码几乎相同。
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 );
最后,我们告诉 Explorer 添加了多少项。
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 2); }
提供飞入式帮助和动词
与以前一样,当 Explorer 需要显示飞入式帮助或获取我们命令之一的动词时,会调用GetCommandString()
方法。此扩展与上一个不同之处在于我们有两个菜单项,因此我们需要检查uCmdID
参数来确定 Explorer 调用我们的是哪个项。
#include <atlconv.h> HRESULT CDLLRegShlExt::GetCommandString ( UINT uCmdID, UINT uFlags, UINT* puReserved, LPSTR szName, UINT cchMax ) { USES_CONVERSION; LPCTSTR szPrompt; 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; }
如果uCmdID
为 0,则我们正在为第一个项(注册)调用。如果为 1,则我们正在为第二个项(注销)调用。确定帮助字符串后,我们将其复制到提供的缓冲区中,必要时先转换为 Unicode。
// 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 ); } return S_OK; }
对于此扩展,我还编写了提供动词的代码。但是,在我进行测试期间,Explorer 从未调用GetCommandString()
来获取动词。我甚至编写了一个测试应用程序,该应用程序在 DLL 上调用ShellExecute()
并尝试使用动词,但这也不起作用。我在此省略了与动词相关的代码,但如果您有兴趣,可以在示例项目中查看。
执行用户的选择
当用户单击我们的菜单项之一时,Explorer 会调用我们的InvokeCommand()
方法。InvokeCommand()
首先检查lpVerb
的高字。如果它非零,那么它就是调用动词的名称。由于我们知道动词无法正常工作(至少在 Win 98 上),因此我们将退出。否则,如果lpVerb
的低字为 0 或 1,我们就知道单击了我们的两个菜单项之一。
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; } }
如果lpVerb
为 0 或 1,我们将创建一个进度对话框(它继承自 ATL 类CDialogImpl
),并将文件名列表传递给它。
所有实际工作都发生在CProgressDlg
类中。其OnInitDialog()
函数初始化列表控件,然后调用CProgressDlg::DoWork()
。DoWork()
迭代在CDLLRegShlExt::Initialize()
中构建的字符串列表,并在每个文件中调用相应的函数。基本代码如下;它不完整,因为为了清晰起见,我省略了错误检查和填充列表控件的部分。它足以演示如何遍历文件名列表并对每个文件进行操作。
void CProgressDlg::DoWork() { HRESULT (STDAPICALLTYPE* pfn)(); string_list::const_iterator it; 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(); it != m_pFileList->end(); 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(); // Omitted: error handling and checks on the return // value of the function we called. } // end for
DoWork()
的其余部分是清理和错误处理。您可以在示例项目中的ProgressDlg.cpp中找到完整代码。
注册外壳扩展
DllReg
扩展操作的是进程内 COM 服务器,因此它应该在 DLL 和 OCX 文件上调用。与第一部分一样,我们可以通过 RGS 脚本DllRegShlExt.rgs
来实现这一点。以下是注册我们的 DLL 作为这些扩展的上下文菜单处理程序的必要脚本。
HKCR { NoRemove dllfile { 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}' } } } }
如果您需要复习,RGS 文件的语法以及NoRemove
和ForceRemove
关键字在第一部分中已得到解释。
与我们之前的扩展一样,在基于 NT 的操作系统上,我们需要将我们的扩展添加到“已批准”扩展列表中。执行此操作的代码位于DllRegisterServer()
和DllUnregisterServer()
函数中。我在这里不展示代码,因为它只是简单的注册表访问,但您可以在示例项目中找到代码。
扩展的实际运行
当您单击我们的菜单项之一时,将显示进度对话框并显示操作结果。
列表控件显示每个文件名以及函数调用是否成功。选择一个文件时,列表下方会显示一条消息,其中包含更多详细信息,以及函数调用失败时的错误描述。
请注意,在上一个屏幕截图中,对话框未使用 XP 主题。如 MSDN 文章 “使用 Windows XP 视觉样式”中所述,我们需要执行两项操作才能使我们的 UI 进行主题化。第一项是告诉操作系统为我们的 DLL 使用通用控件 6 版本,方法是将清单放在资源中。您可以从上面的 MSDN 文章中复制必要的清单 XML,并将其保存到项目res子目录中的一个名为dllreg.manifest的文件中。接下来,将此行添加到资源 includes:
ISOLATIONAWARE_MANIFEST_RESOURCE_ID RT_MANIFEST "res\\dllreg.manifest"
然后在stdafx.h中,在所有 includes 之前添加此行:
#define ISOLATION_AWARE_ENABLED 1
截至 2006 年 5 月,MSDN 文章称该符号称为SIDEBYSIDE_COMMONCONTROLS
,但在我的 SDK 中,仅使用ISOLATION_AWARE_ENABLED
。如果您有较新的 SDK 并且ISOLATION_AWARE_ENABLED
对您不起作用,请尝试SIDEBYSIDE_COMMONCONTROLS
。
进行这些更改并重新构建后,对话框现在会遵循活动主题。
注册扩展的其他方式
到目前为止,我们的扩展仅针对特定文件类型调用。可以通过在HKCR\*
键下注册为上下文菜单处理程序来让 shell 为任何文件调用我们的扩展。
HKCR { NoRemove * { NoRemove shellex { NoRemove ContextMenuHandlers { ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}' } } } }
HKCR\*
键列出了对所有文件调用的 shell 扩展。请注意,文档称扩展也会为任何 shell 对象(包括文件、目录、虚拟文件夹、控制面板项等)调用,但这与我在测试期间看到的行为不同。扩展仅针对文件系统中的文件调用。
在 shell 版本 4.71+ 中,还有一个名为HKCR\AllFileSystemObjects
的键。如果我们在此键下注册,我们的扩展将被调用以响应文件系统中的所有文件和目录,但不包括根目录。(对于被调用以响应根目录的扩展,它们会在HKCR\Drive
下注册。)但是,在某些 Windows 版本上,在此键下注册时会出现一些奇怪的行为。例如,在 Windows 98 上,DllReg 菜单项最终与发送到项混合在一起。
这在 XP 上不是问题。
您还可以编写一个操作目录的上下文菜单扩展。有关此类扩展的示例,请参阅我的文章 用于清理编译器临时文件的实用程序。
最后,在 shell 版本 4.71+ 中,您可以让上下文菜单扩展在用户右键单击查看目录(包括桌面)的 Explorer 窗口的背景时被调用。要让您的扩展像这样被调用,请在HKCR\Directory\Background\shellex\ContextMenuHandlers
下注册它。使用此方法,您可以将自己的菜单项添加到桌面上下文菜单或任何其他目录的菜单中。但是,传递给IShellExtInit::Initialize()
的参数有所不同,因此我可能将在以后的文章中介绍此主题。
未完待续
在第三部分中,我们将研究一种新型扩展——QueryInfo handler,它显示 shell 对象的弹出描述。我还将展示如何在 shell 扩展中使用 MFC。
版权和许可
本文是版权材料,©2003-2006 Michael Dunn。我知道这无法阻止人们在网络上随意复制它,但我必须说出来。如果您有兴趣翻译本文,请给我发电子邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译情况,以便我可以在此处发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码能使所有人受益。(我不会将文章本身设为公共领域,因为让文章仅在 CodeProject 上提供有助于提高我的知名度和 CodeProject 网站的流量。)如果您在自己的应用程序中使用演示代码,我很乐意收到电子邮件通知(只是为了满足我对人们是否从我的代码中受益的好奇心),但这不是必需的。在您自己的源代码中署名也受到赞赏,但不是必需的。
修订历史
- 2000 年 3 月 29 日:文章首次发布。
- 2000 年 6 月 6 日:更新了某些内容。;)
- 2006 年 5 月 14 日:更新以涵盖 VC 7.1 中的更改。