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






4.91/5 (21投票s)
2000 年 3 月 30 日

569998

4141
一个关于编写 Shell 扩展的教程,该扩展可以显示文件的弹出信息。
目录
引言
在指南的第一部分和第二部分中,我展示了如何编写上下文菜单扩展。在第三部分中,我将演示一种不同的扩展类型,解释如何与 Shell 共享内存,并展示如何将 MFC 与 ATL 一起使用。本部分假设您已从前两部分理解了 Shell 扩展的基础知识,并熟悉 MFC。
Active Desktop Shell 引入了一项新功能:当鼠标悬停在某些对象上时,会显示一个描述性的工具提示。例如,将鼠标悬停在我的文档上会显示此工具提示
其他对象,如“我的电脑”和“控制面板”也有类似的工具提示。这些文本被称为信息提示 (infotips),因为它们是提供有关鼠标悬停在其上的文件、文件夹或对象的信息的工具提示。我们可以为 Shell 中的其他对象提供自己的信息提示文本,使用信息提示扩展。一个您可能已经见过的例子是 WinZip,它会显示压缩文件的内容
本文的示例扩展将是一个快速文本文件查看器 - 它将显示文件的第一行以及文件的总大小。当用户将鼠标悬停在 .TXT
文件上时,此信息将显示在信息提示中。
请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。
使用 AppWizard 开始
像在之前的文章中一样,运行 AppWizard 并创建一个新的 ATL COM 应用程序。我们将它命名为 *TxtInfo*。由于这次我们要使用 MFC,请勾选 *支持 MFC* 复选框,然后单击 *完成*。
要向 DLL 添加 COM 对象,请转到 ClassView 树,右键单击 *TxtInfo classes* 项,然后选择 *新建 ATL 对象*。(在 VC 7 中,右键单击该项并选择 *添加|添加类*。)像以前一样,在向导中选择“简单对象”,并将对象命名为 *TxtInfoShlExt*。这将创建一个 C++ 类 CTxtInfoShlExt
,它将实现该扩展。
如果您查看 ClassView 树,您会看到一个 CTxtInfoApp
类,它继承自 CWinApp
。这个类的存在以及全局变量 theApp
,使得 MFC 可供我们使用,就像我们编写一个普通的 MFC DLL 而没有任何 ATL 一样。
初始化接口
在之前文章的上下文菜单扩展中,我们实现了 IShellExtInit
接口,这是 Explorer 初始化我们对象的方式。还有另一个用于某些 Shell 扩展的初始化接口,IPersistFile
,这是信息提示扩展使用的接口。为什么会有这种区别?如果您还记得,IShellExtInit::Initialize()
接收一个 IDataObject
指针,它可以使用该指针枚举所有选定的文件。只能操作单个文件的扩展使用 IPersistFile
。由于鼠标一次不能悬停在多个对象上,信息提示扩展一次只能处理一个文件,因此它使用 IPersistFile
。
我们首先需要将 IPersistFile
添加到 CTxtInfoShlExt
实现的接口列表中。打开 *TxtInfoShlExt.h*,并添加粗体显示的行。
#include <comdef.h> #include <shlobj.h> class CTxtInfoShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>, public IPersistFile { BEGIN_COM_MAP(CTxtInfoShlExt) COM_INTERFACE_ENTRY(IPersistFile) END_COM_MAP()
我们还需要一个变量来存储 Explorer 在初始化期间提供给我们的文件名。
protected:
CString m_sFilename;
请注意,我们可以在任何需要的地方使用 MFC 对象。
如果您查阅 IPersistFile
的文档,您会看到大量方法。幸运的是,对于 Shell 扩展而言,我们只需要实现 Load()
方法,而忽略其他方法。这是 IPersistFile
方法的原型。
class CTxtInfoShlExt : ... { // ... public: STDMETHODIMP GetClassID(LPCLSID) { return E_NOTIMPL; } STDMETHODIMP IsDirty() { return E_NOTIMPL; } STDMETHODIMP Load(LPCOLESTR, DWORD); STDMETHODIMP Save(LPCOLESTR, BOOL) { return E_NOTIMPL; } STDMETHODIMP SaveCompleted(LPCOLESTR) { return E_NOTIMPL; } STDMETHODIMP GetCurFile(LPOLESTR*) { return E_NOTIMPL; } };
除了 Load()
之外的所有方法都返回 E_NOTIMPL
,表示我们没有实现它们。为了使情况更好,我们的 Load()
实现也非常简单。我们只是存储 Explorer 传递给我们的文件名。这是鼠标悬停的文件名的完整路径。
HRESULT CTxtInfoShlExt::Load ( LPCOLESTR wszFilename, DWORD dwMode ) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC // Let CString convert the filename to ANSI. m_sFilename = wszFilename; return S_OK; }
函数中的第一行至关重要。AFX_MANAGE_STATE
宏对于 MFC 的正常工作是必需的。因为我们的 DLL 是由一个非 MFC 应用程序加载的,所以每个使用 MFC 的导出函数都必须手动初始化 MFC。如果您不包含这一行,许多 MFC 函数(主要是与资源相关的函数)将无法正常工作或出现断言失败。
文件名存储在 m_sFilename
中,供以后使用。请注意,我利用了 CString
的赋值运算符将字符串转换为 ANSI,前提是 DLL 是以 ANSI 构建的。
创建工具提示文本
在 Explorer 调用我们的 Load()
方法后,它会调用 QueryInterface()
来获取另一个接口:IQueryInfo
。IQueryInfo
是一个相当简单的接口,只有两个方法(实际上只使用一个)。再次打开 *TxtInfoShlExt.h*,并添加此处列出的粗体显示的行。
class CTxtInfoShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>, public IPersistFile, public IQueryInfo { BEGIN_COM_MAP(CTxtInfoShlExt) COM_INTERFACE_ENTRY(IPersistFile) COM_INTERFACE_ENTRY(IQueryInfo) END_COM_MAP()
然后添加 IQueryInfo
方法的原型。
class CTxtInfoShlExt : ... { // ... STDMETHODIMP GetInfoFlags(DWORD*) { return E_NOTIMPL; } STDMETHODIMP GetInfoTip(DWORD, LPWSTR*); };
GetInfoFlags()
方法不使用,所以我们只需要返回 E_NOTIMPL
。GetInfoTip()
是我们向 Explorer 返回文本以在工具提示中显示的地方。首先,是开头那些枯燥的部分。
HRESULT CTxtInfoShlExt::GetInfoTip (
DWORD dwFlags, LPWSTR* ppwszTip )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC
USES_CONVERSION;
LPMALLOC pMalloc;
CStdioFile file;
DWORD dwFileSize;
CString sFirstLine;
BOOL bReadLine;
CString sTooltip;
同样,AFX_MANAGE_STATE
调用是第一个,用于初始化 MFC。这必须在函数的开头完成,甚至在变量声明之前,因为局部变量的构造函数可能会调用 MFC 函数。
dwFlags
目前不使用。ppwszTip
是一个指向 LPWSTR
的指针,我们将将其设置为指向我们必须分配的一个缓冲区。
首先,我们将尝试以只读方式打开文件。我们知道文件名,因为它是在之前的 Load()
函数中存储的。
if ( !file.Open(m_sFilename, CFile::modeRead|CFile::shareDenyWrite) ) return E_FAIL;
现在,由于我们需要使用 Shell 的内存分配器来分配缓冲区,因此我们需要一个 IMalloc
接口指针。我们通过调用 SHGetMalloc()
函数来获得它。
if ( FAILED(SHGetMalloc(&pMalloc)) ) return E_FAIL;
稍后我将详细介绍 IMalloc
。下一步是获取文件大小,并读取第一行。
// Get the size of the file. dwFileSize = file.GetLength(); // Read in the first line from the file. bReadLine = file.ReadString ( sFirstLine );
bReadLine
通常会是 TRUE,除非文件无法访问或长度为 0 字节。下一步是创建工具提示的第一部分,其中列出了文件大小。
sTooltip.Format ( _T("File size: %lu"), dwFileSize );
现在,如果我们可以读取文件的第一行,我们就将其添加到工具提示中。
if ( bReadLine ) { sTooltip += _T("\n"); sTooltip += sFirstLine; }
现在我们有了完整的工具提示,我们需要分配一个缓冲区。这就是我们使用 IMalloc
的地方。SHGetMalloc()
返回的指针是 Shell 的 IMalloc
接口的副本。我们用该接口分配的任何内存都驻留在 Shell 的进程空间中,因此 Shell 可以使用它。更重要的是,Shell 也可以释放它。所以我们所做的是分配缓冲区,然后就忘记它。Shell 在使用完内存后会将其释放。
还需要注意的一点是,我们返回给 Shell 的字符串必须是 Unicode 格式。这就是为什么下面的 Alloc()
调用中的计算乘以 sizeof(wchar_t)
;仅仅为 lstrlen(sToolTip)
分配内存只会分配所需内存量的一半。
*ppwszTip = (LPWSTR) pMalloc->Alloc ( (1 + lstrlen(sTooltip)) * sizeof(wchar_t) ); if ( NULL == *ppwszTip ) { pMalloc->Release(); return E_OUTOFMEMORY; } // Use the Unicode string copy function to put the tooltip text in the buffer. wcscpy ( *ppwszTip, T2COLE(LPCTSTR(sTooltip)) );
最后一步是释放我们之前获得的 IMalloc
接口。
pMalloc->Release();
return S_OK;
}
就这样!Explorer 获取 *ppwszTip
中的字符串并在工具提示中显示它。
注册外壳扩展
信息提示扩展的注册方式与上下文菜单扩展略有不同。我们的扩展程序在 HKEY_CLASSES_ROOT
的一个子项下注册,该子项的名称是我们想要处理的文件扩展名。在这种情况下,它是 HKCR\.txt
。但等等,还有更奇怪的!您可能会认为 ShellEx
子项的名称应该是“TooltipHandlers”这样更具逻辑性的名称。不对!该键的名称是 "{00021500-0000-0000-C000-000000000046}"。
我认为 Microsoft 正在试图偷偷地将一些 Shell 扩展隐藏起来!如果您在注册表中进行探索,您会发现其他名为 GUID 的 ShellEx
子项。上面的 GUID 恰好是 IQueryInfo
的 GUID。
这是告诉 Explorer 在 .TXT
文件上调用我们扩展的 RGS 脚本。
HKCR { NoRemove .txt { NoRemove shellex { {00021500-0000-0000-C000-000000000046} = s '{F4D78AE1-05AB-11D4-8D3B-444553540000}' } } }
您可以通过复制上述代码片段,并将“.txt”替换为您想要的任何扩展名,轻松地使该扩展程序对其他扩展名生效。不幸的是,您不能在 *
或 AllFileSystemObjects
键下注册信息提示扩展程序以使其对所有文件生效。
与我们之前的扩展一样,在基于 NT 的操作系统上,我们需要将我们的扩展添加到系统的批准扩展列表中。执行此操作的代码位于示例项目中的 DllRegisterServer()
和 DllUnregisterServer()
函数中。
待续...
在下一部分(第四部分)中,我们将回到上下文菜单的世界,并了解一种新的扩展类型,即拖放处理程序。我们还将看到更多 MFC 的用法。
版权和许可
本文受版权保护,©2000-2006 Michael Dunn。我知道这不会阻止人们在网上到处复制它,但我还是必须说出来。如果您有兴趣翻译本文,请给我发电子邮件告知我。我不认为我会拒绝任何人进行翻译的许可,只是希望知道有翻译的存在,以便我在此处发布链接。
伴随本文的演示代码已发布到公共领域。我这样发布是为了让代码惠及所有人。(我不会将文章本身公开,因为只有在 CodeProject 上提供文章有助于我个人以及 CodeProject 网站的可见度。)如果您在自己的应用程序中使用演示代码,如果您能发邮件告知我将不胜感激(仅为满足我对人们是否受益于我的代码的好奇心),但不是必需的。在您自己的源代码中注明出处也同样受欢迎,但不是必需的。
修订历史
2000 年 3 月 30 日:文章首次发布。
2000 年 6 月 6 日:更新了某些内容。;)
2006 年 5 月 17 日:更新以涵盖 VC 7.1 中的更改,清理了代码片段。