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





5.00/5 (31投票s)
2000年4月20日

451821

5953
关于编写可用于“发送到”菜单的 Shell 扩展的教程。
目录
引言
在本指南的第六部分,我将介绍一种不太常用的 Shell 扩展类型:拖放处理程序 (drop handler)。这种类型可用于为 Explorer 添加拖放功能,其中被拖放到的文件(目标文件)决定了被调用的扩展。
本文假设您已经了解 Shell 扩展的基础知识,并熟悉用于与 Shell 交互的 MFC 类。如果您需要回顾 MFC 类,应该阅读第四部分,因为本文将使用相同的技术。
请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。
在第四部分中,我讨论了拖放处理程序,它在右键拖放操作期间被调用。Explorer 还允许我们编写一种扩展,它在左键拖放操作期间被调用,其中扩展操作的是被拖放到的文件。例如,WinZip 包含一个拖放处理程序,允许您通过将文件拖放到 zip 文件上来将文件添加到 zip 中。当您将文件拖到 zip 文件上时,Explorer 会通过突出显示 zip 文件并更改鼠标光标(使其带有一个 + 图标)来指示该 zip 文件是目标文件。
如果没有拖放处理程序,当您尝试将文件拖到 zip 文件上时,将不会发生任何事情。
拖放处理程序只有在您拥有自己的文件类型时才像这样有用,就像 WinZip 所做的那样。拖放处理程序更有趣的用途是将项目添加到“*发送到*”菜单。*“发送到”*菜单显示*“SendTo”*菜单的内容(在 Windows 9x 上,此目录位于 Windows 目录下;在 NT 类操作系统上,此目录位于用户配置文件目录下)。在早期版本的 Windows 中,“SendTo
”目录仅包含快捷方式,但较新版本(以及 Shell Power Toys 等其他应用程序)会添加其他几个项目,如下所示:
如果您不明白拖放处理程序如何发挥作用,请查看“SendTo
”目录的目录列表。
12/23/05 3:39a 129 3½ Floppy (A).lnk 12/28/05 1:42p 0 Desktop (create shortcut).DeskLink 12/28/05 1:42p 0 Mail Recipient.MAPIMail 12/23/05 12:31p 0 My Documents.mydocs 5/25/06 11:05a 1,267 Notepad.lnk
注意到那些奇怪的扩展名,如“.DeskLink”。那些零字节文件是为了使项目出现在*“发送到”*菜单中,并且扩展名在注册表中列出。然而,它们不是普通的文件关联,因为这些文件没有*打开*或*打印*等动词。它们*有*的是拖放处理程序。当您在*“发送到”*菜单中选择其中一个项目时,Explorer 会调用关联的拖放处理程序。这里是菜单的另一张视图,其中标出了目标文件:
本文的示例项目将是旧的“发送到任意文件夹”Powertoy 的克隆 - 它将把文件复制或移动到您计算机上的任何目录。
初始化接口
您现在应该熟悉设置步骤了,所以我将跳过通过 VC 向导的说明。如果您正在按照向导进行操作,请创建一个名为*SendToClone*的新的 ATL COM 应用程序,并使用名为CSendToShlExt
的 C++ 实现类。
由于拖放处理程序是针对单个文件(被拖放的文件)调用的,因此初始化通过IPersistFile
接口完成。(回想一下,IPersistFile
用于只能操作单个文件的扩展。)IPersistFile
有很多方法,但在 Shell 扩展中,只需要实现Load()
方法。
我们需要将IPersistFile
添加到CSendToShlExt
实现的接口列表中。打开*SendToShlExt.h*并添加此处以粗体显示的行。
#include <comdef.h> #include <shlobj.h> class CSendToShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>, public IPersistFile { BEGIN_COM_MAP(CSendToShlExt) COM_INTERFACE_ENTRY(IPersistFile) END_COM_MAP() public: // IPersistFile STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; } STDMETHOD(IsDirty)() { return E_NOTIMPL; } STDMETHOD(Load)(LPCOLESTR, DWORD) { return S_OK; } STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; } STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; } STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; }
请注意,Load()
除了返回S_OK
之外,什么也不做。Load()
方法接收被拖放文件的完整路径,但对于此扩展,我们不关心该文件名,因此可以忽略它。
参与拖放操作
为了完成其工作,我们的扩展必须与拖放源(即 Explorer 本身)进行通信。我们的扩展会获取被拖动文件的列表,并告诉 Explorer 用户是否接受在放置这些文件时。这种通信自然是通过一个接口进行的:IDropTarget
。IDropTarget
的方法是:
DragEnter()
:当用户首次拖到某个文件上时调用。返回值告诉 Explorer 扩展是否接受放置的文件。DragOver()
:在 Shell 扩展中不调用。DragLeave()
:当用户在不放置的情况下将鼠标移出目标文件时调用。Drop()
:当用户将文件放置到目标文件上时调用。这是 Shell 扩展执行其工作的地方。
要将IDropTarget
添加到CSendToShlExt
,请打开*SendToShlExt.h*并添加此处以粗体显示的行。
class CSendToShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
public IPersistFile,
public IDropTarget
{
BEGIN_COM_MAP(CSendToShlExt)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IDropTarget)
END_COM_MAP()
public:
// IDropTarget
STDMETHODIMP DragEnter(IDataObject* pDataObj, DWORD grfKeyState,
POINTL pt, DWORD* pdwEffect);
STDMETHODIMP DragOver(DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
{ return E_NOTIMPL; }
STDMETHODIMP DragLeave();
STDMETHODIMP Drop(IDataObject* pDataObj, DWORD grfKeyState,
POINTL pt, DWORD* pdwEffect);
protected:
CStringList m_lsDroppedFiles;
}
与早期扩展一样,我们将使用字符串列表来存储被拖动文件的名称。DragOver()
方法不需要实现,因为它不被调用。我将在下面介绍其他三个方法。
DragEnter()
DragEnter()
的原型是:
HRESULT IDropTarget::DragEnter ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect );
pDataObj
是一个IDataObject
接口,我们可以用它来枚举被拖动的文件。grfKeyState
是一组标志,指示按下了哪些 Shift 键和鼠标按钮。pt
是一个POINTL
结构(碰巧与POINT
相同),它保存鼠标坐标。pdwEffect
是一个指向DWORD
的指针,我们将在此指针中返回一个值,该值告诉 Explorer 我们是否接受拖放,如果接受,则应覆盖鼠标光标的图标类型。
正如我之前提到的,DragEnter()
通常在用户首次拖到目标文件上时调用。但是,当用户单击*“发送到”*菜单项时也会调用它,因此即使技术上没有发生拖放,我们仍然可以在DragEnter()
中完成所有工作。
我们的DragEnter()
实现将使用字符串列表填充被拖动的文件名。此扩展将接受所有文件或目录,因为任何文件系统对象都可以被复制或移动。
DragEnter()
的开头是您现在应该熟悉的-我们将COleDataObject
附加到IDataObject
接口,并枚举被拖动的文件。
STDMETHODIMP CSendToShlExt::DragEnter ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect ) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC COleDataObject dataobj; TCHAR szItem[MAX_PATH]; UINT uNumFiles; HGLOBAL hglobal; HDROP hdrop; dataobj.Attach ( pDataObj, FALSE ); // Read the list of items from the data object. They're stored in HDROP // form, so just get the HDROP handle and then use the drag 'n' drop APIs // on it. hglobal = dataobj.GetGlobalData ( CF_HDROP ); if ( NULL != hglobal ) { hdrop = (HDROP) GlobalLock ( hglobal ); uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 ); for ( UINT uFile = 0; uFile < uNumFiles; uFile++ ) { if ( 0 != DragQueryFile ( hdrop, uFile, szItem, MAX_PATH )) m_lsDroppedFiles.AddTail ( szItem ); } GlobalUnlock ( hglobal ); }
现在是时候在pdwEffect
中返回值了。我们可以返回的效果是:
DROPEFFECT_COPY
:告诉 Explorer 被拖动的文件将被我们的扩展*复制*。DROPEFFECT_MOVE
:告诉 Explorer 被拖动的文件将被我们的扩展*移动*。DROPEFFECT_LINK
:告诉 Explorer 被拖动的文件将被我们的扩展*链接*。DROPEFFECT_NONE
:告诉 Explorer 我们不接受被拖动的文件。
我们只返回DROPEFFECT_COPY
。我们不能返回DROPEFFECT_MOVE
,因为这会让 Explorer*删除*被放置的文件。我们可以返回DROPEFFECT_LINK
,但光标将带有指示快捷方式的小箭头,这对用户来说是误导性的。如果文件列表为空(如果我们无法读取剪贴板,则会发生这种情况),我们返回DROPEFFECT_NONE
来告诉 Explorer 我们不接受拖放。
if ( m_lsDroppedFiles.GetCount() > 0 ) { *pdwEffect = DROPEFFECT_COPY; return S_OK; } else { *pdwEffect = DROPEFFECT_NONE; return E_INVALIDARG; } }
DragLeave()
如果用户在不放置的情况下将鼠标移出我们的目标文件,则会调用DragLeave()
。*“发送到”*菜单不使用此方法,但如果您打开*“发送到”*目录的 Explorer 窗口并将文件拖到该目录中的文件上,则会调用此方法。我们没有任何清理任务,所以我们只需要返回S_OK
。
STDMETHODIMP CSendToShlExt::DragLeave() { return S_OK; }
Drop()
如果用户选择我们的*“发送到”*菜单项,Explorer 会调用Drop()
,其原型如下:
HRESULT IDropTarget::Drop ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect );
前三个参数与DragEnter()
相同。Drop()
应通过pdwEffect
参数返回操作的最终效果。我们的Drop()
函数创建主对话框并将文件名列表传递给它。对话框执行所有工作,当DoModal()
返回后,我们设置最终的拖放效果。
STDMETHODIMP CSendToShlExt::Drop (
IDataObject* pDataObj,
DWORD grfKeyState,
POINTL pt,
DWORD* pdwEffect )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC
CSendToCloneDlg dlg ( &m_lsDroppedFiles );
dlg.DoModal();
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
对话框看起来是这样的:
这是一个相当直接的 MFC 对话框,您可以在*SendToCloneDlg.cpp*文件中找到源代码。我使用了我文章“CShellFileOp - SHFileOperation 的封装器”中的CShellFileOp
类来执行实际的移动和复制操作。
与第二部分中的扩展一样,我们需要向 DLL 的资源中添加一个清单,以便在 XP 上为我们的对话框应用主题。但是,在这种情况下,仅添加清单是不够的,因为创建和管理对话框的代码在 MFC 中。MFC 没有定义ISOLATION_AWARE_ENABLED
符号进行编译,因此代码不使用连接主题所必需的IsolationAwareXxx
包装器。
在这个新闻组线程中,Dave Anderson 的帖子很好地解释了这种情况。总而言之,在显示对话框之前,我们需要使用*激活上下文* API,以便 Windows 使用我们的清单并为我们的对话框使用通用控件的第 6 版。这段代码封装在CActCtx
类中,并在我们的Drop()
方法中使用:
STDMETHODIMP CSendToShlExt::Drop(...) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC CSendToCloneDlg dlg ( &m_lsDroppedFiles ); CAxtCtx ctx; dlg.DoModal(); *pdwEffect = DROPEFFECT_COPY; return S_OK; }
添加此代码后,对话框将正确应用主题。
但是等等 - 我们如何告诉 Explorer 关于我们的拖放处理程序?我们又如何将一个项目添加到*“发送到”*菜单中?我将在下一节中进行解释。
注册扩展
注册拖放处理程序与其他类型的扩展略有不同,因为它需要在HKEY_CLASSES_ROOT
下创建一个新的关联。AppWizard 生成的 RGS 脚本如下所示。您应该添加或更改的部分以粗体显示。
HKCR { .SendToClone = s 'CLSID\{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}' NoRemove CLSID { ForceRemove {B7F3240E-0E29-11D4-8D3B-80CD3621FB09} = s 'Send To Any Folder Clone' { InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Apartment' } val NeverShowExt = s '' DefaultIcon = s '%MODULE%,0' shellex { DropHandler = s '{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}' } } } }
第一行是进行关联的那一行。它创建了一个新的扩展.SendToClone
,该扩展将由我们的目标文件使用。请注意,.SendToClone
键的默认值前缀是“CLSID\”。这告诉 Explorer 描述关联的数据位于HKCR\CLSID
下的一个键中。对于更传统的关联,这些内容存储在HKEY_CLASSES_ROOT
下的一个键中(例如,.txt
键指向HKCR\txtfile
键),但将拖放处理程序的关联数据存储在其CLSID
键下似乎是一种约定,以便将所有数据保留在一个地方。
“Send To Any Folder Clone”字符串是在 Explorer 中浏览到*SendTo
*目录时显示的文件类型描述。创建NeverShowExt
值是为了告诉 Explorer 不应显示“.SendToClone”扩展名。DefaultIcon
键列出了要用于.SendToClone
文件的图标位置。最后,我们有了熟悉的shellex
键及其DropHandler
子键。由于一个文件类型只能有一个拖放处理程序,因此处理程序的 GUID 直接存储在DropHandler
键中,而不是列在DropHandler
下的子键中。
最后一个细节是在*SendTo
*目录中创建一个文件,以便我们的菜单项出现。我们可以在DllRegisterServer()
中执行此操作,并在DllUnregisterServer()
中删除文件。以下是创建文件的代码:
LPITEMIDLIST pidl;
TCHAR szSendtoPath[MAX_PATH];
HANDLE hFile;
LPMALLOC pMalloc;
if ( SUCCEEDED( SHGetSpecialFolderLocation ( NULL, CSIDL_SENDTO, &pidl ) ))
{
if ( SHGetPathFromIDList ( pidl, szSendtoPath ) )
{
PathAppend ( szSendtoPath, _T("Some other folder.SendToClone") );
hFile = CreateFile ( szSendtoPath, GENERIC_WRITE, FILE_SHARE_READ,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
CloseHandle ( hFile );
}
if ( SUCCEEDED( SHGetMalloc ( &pMalloc ) ))
{
pMalloc->Free ( pidl );
pMalloc->Release();
}
}
这是新的*“发送到”*菜单项的外观,以及我们自己的项目:
DllUnregisterServer()
具有删除文件的类似代码。上面的代码将在所有 Windows 版本(嗯,所有 4 及更高版本)上工作。如果您知道代码将在仅 shell 版本 4.71 及更高版本上运行,则可以使用SHGetSpecialFolderPath()
函数而不是SHGetSpecialFolderLocation()
。
与往常一样,在 NT 类操作系统上,我们需要将我们的扩展添加到“已批准”扩展列表中。此代码位于示例项目中的DllRegisterServer()
和DllUnregisterServer()
函数中。
待续...
在第七部分中,我将回答一些读者的请求,并演示两种新的上下文菜单扩展变体。
版权和许可
本文是版权材料,©2000-2006 Michael Dunn。我知道这无法阻止人们在网络上到处复制它,但我必须这么说。如果您有兴趣翻译本文,请给我发电子邮件告知我。我预计不会拒绝任何人翻译的许可,只是想了解翻译情况,以便在此处发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码能够使每个人受益。(我不会将本文本身公开,因为只在 CodeProject 上提供本文有助于提高我的知名度以及 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,我会很感激您发送电子邮件告知我(只是为了满足我对人们是否从我的代码中受益的好奇心),但这并非必需。在您自己的源代码中注明出处也值得赞赏,但不是必需的。
修订历史
2000年4月20日:文章首次发布。
2000 年 6 月 6 日:更新了某些内容。;)
2006年5月26日:更新以涵盖 VC 7.1 的更改,清理代码片段,示例项目在 XP 上应用主题。