65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (31投票s)

2000年4月20日

viewsIcon

451821

downloadIcon

5953

关于编写可用于“发送到”菜单的 Shell 扩展的教程。

目录

引言

在本指南的第六部分,我将介绍一种不太常用的 Shell 扩展类型:拖放处理程序 (drop handler)。这种类型可用于为 Explorer 添加拖放功能,其中被拖放到的文件(目标文件)决定了被调用的扩展。

本文假设您已经了解 Shell 扩展的基础知识,并熟悉用于与 Shell 交互的 MFC 类。如果您需要回顾 MFC 类,应该阅读第四部分,因为本文将使用相同的技术。

请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。

第四部分中,我讨论了拖放处理程序,它在右键拖放操作期间被调用。Explorer 还允许我们编写一种扩展,它在左键拖放操作期间被调用,其中扩展操作的是被拖放到的文件。例如,WinZip 包含一个拖放处理程序,允许您通过将文件拖放到 zip 文件上来将文件添加到 zip 中。当您将文件拖到 zip 文件上时,Explorer 会通过突出显示 zip 文件并更改鼠标光标(使其带有一个 + 图标)来指示该 zip 文件是目标文件。

 [Dropping on a zip - 3K]

如果没有拖放处理程序,当您尝试将文件拖到 zip 文件上时,将不会发生任何事情。

 [Dropping on a zip w/o a drop handler - 3K]

拖放处理程序只有在您拥有自己的文件类型时才像这样有用,就像 WinZip 所做的那样。拖放处理程序更有趣的用途是将项目添加到“*发送到*”菜单。*“发送到”*菜单显示*“SendTo”*菜单的内容(在 Windows 9x 上,此目录位于 Windows 目录下;在 NT 类操作系统上,此目录位于用户配置文件目录下)。在早期版本的 Windows 中,“SendTo”目录仅包含快捷方式,但较新版本(以及 Shell Power Toys 等其他应用程序)会添加其他几个项目,如下所示:

 [Sample Send To menu - 3K]

如果您不明白拖放处理程序如何发挥作用,请查看“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 会调用关联的拖放处理程序。这里是菜单的另一张视图,其中标出了目标文件:

 [Send To menu again - 5K]

本文的示例项目将是旧的“发送到任意文件夹”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 用户是否接受在放置这些文件时。这种通信自然是通过一个接口进行的:IDropTargetIDropTarget的方法是:

  • 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;
}

对话框看起来是这样的:

 [Send to dialog - 14K]

这是一个相当直接的 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;
}

添加此代码后,对话框将正确应用主题。

 [Send to dialog (themed) - 16K]

但是等等 - 我们如何告诉 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();
      }
    }

这是新的*“发送到”*菜单项的外观,以及我们自己的项目:

 [New Send To item - 3K]

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 上应用主题。

系列导航:« 第五部分 | 第七部分 »

© . All rights reserved.