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





5.00/5 (21投票s)
2000年4月3日

319341

4377
一篇关于编写提供自定义拖放功能的Shell扩展的教程。
目录
引言
在指南的第一和第二部分中,我展示了如何编写当用户右键单击特定类型文件时调用的上下文菜单扩展。在这一部分中,我将演示一种不同类型的上下文菜单扩展,即拖放处理程序,它会向右键拖放操作显示的上下文菜单中添加项目。我还会提供更多在扩展中使用MFC的示例。
第四部分假设您了解Shell扩展的基础知识,并熟悉MFC。这个特定的扩展是一个真正的实用程序,可以在Windows 2000及更高版本上创建硬链接,但即使您使用的是旧版Windows,您仍然可以跟着操作。
正如每个高级用户都知道(但很少普通用户知道)的那样,您可以使用鼠标右键在Explorer中拖放项目。当您释放按钮时,Explorer会显示一个上下文菜单,列出所有可用的操作。通常,这些操作是移动、复制和创建快捷方式。
Explorer允许我们通过使用拖放处理程序向此菜单添加项目。当发生任何右键拖放操作时,会调用此类型的扩展,并且扩展可以根据需要添加菜单项。WinZip中就有一个拖放处理程序的示例。当您右键拖动一个压缩文件时,WinZip会向上下文菜单添加以下内容:
WinZip的扩展适用于任何右键拖放操作,但只有当拖动的是压缩文件时,它才会添加菜单项。
本文的示例扩展将使用Windows 2000中添加的API CreateHardLink()
,在NTFS卷上创建文件的硬链接。我们将在上下文菜单中添加一个用于创建链接的项目,以便用户可以像创建常规快捷方式一样创建硬链接。
请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。
使用 AppWizard 开始
运行AppWizard并创建一个新的ATL COM应用程序。我们将其命名为HardLink。我们将使用MFC,因此勾选支持MFC复选框,然后单击完成。要向DLL添加COM对象,请转到ClassView树,右键单击HardLink classes项,然后选择New ATL Object。(在VC 7中,右键单击该项并选择添加|添加类。)像以前一样,在向导中选择Simple Object,并使用名称HardLinkShlExt作为对象。这将创建一个C++类CHardLinkShlExt
,它将实现扩展。
初始化接口
与我们早期的上下文菜单扩展一样,Explorer使用IShellExtInit
接口初始化我们。我们首先需要将IShellExtInit
添加到CHardLinkShlExt
实现的接口列表中。打开HardLinkShlExt.h,并添加此处用粗体列出的行:
#include <comdef.h> #include <shlobj.h> class CHardLinkShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CHardLinkShlExt, &CLSID_HarkLinkShlExt>, public IShellExtInit { BEGIN_COM_MAP(CHardLinkShlExt) COM_INTERFACE_ENTRY(IShellExtInit) END_COM_MAP() public: // IShellExtInit STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
我们还需要一些变量来保存位图和被拖动文件的名称:
protected: CBitmap m_bitmap; TCHAR m_szFolderDroppedIn[MAX_PATH]; CStringList m_lsDroppedFiles;
此外,我们还需要向stdafx.h添加一些#define
s,以使CreateHardLink()
和shlwapi.dll函数原型可见:
#define WINVER 0x0500 #define _WIN32_WINNT 0x0500 #define _WIN32_IE 0x0400
将WINVER
定义为0x0500可以启用Win 98和2000特有的功能,将_WIN32_WINNT
定义为0x0500可以启用Win 2000特有的NT功能。将_WIN32_IE
定义为0x0400可以启用IE 4引入的功能。
现在,转到Initialize()
方法。这一次,我将展示如何使用MFC访问被拖动文件的列表。MFC有一个类COleDataObject
,它封装了IDataObject
接口。以前,我们必须直接调用IDataObject
方法。但幸运的是,MFC让这项工作对我们来说更容易一些。以下是Initialize()
的原型,以帮助您回忆:
HRESULT IShellExtInit::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID );
对于拖放扩展,pidlFolder
是项目被拖放到文件夹的PIDL。(我稍后会详细介绍PIDL。)pDataObj
是一个IDataObject
接口,我们可以使用它枚举所有被拖放的项目。hProgID
是我们Shell扩展在HKEY_CLASSES_ROOT
下的键的打开HKEY
。
我们的第一步是为菜单项加载位图。然后,我们将一个COleDataObject
变量附加到IDataObject
接口。
HRESULT CHardLinkShlExt::Initialize (
LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj,
HKEY hProgID )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC
COleDataObject dataobj;
HGLOBAL hglobal;
HDROP hdrop;
TCHAR szRoot[MAX_PATH];
TCHAR szFileSystemName[256];
TCHAR szFile[MAX_PATH];
UINT uFile, uNumFiles;
m_bitmap.LoadBitmap ( IDB_LINKBITMAP );
dataobj.Attach ( pDataObj, FALSE );
将FALSE
作为第二个参数传递给Attach()
意味着在dataobj
变量被销毁时不要释放IDataObject
接口。下一步是获取项目被拖放到目录。我们有这个目录的PIDL,但是我们如何获取路径呢?是时候插入一个小插曲了……
"PIDL" 是 pointer to an **ID** **l**ist 的缩写。PIDL 是一种在 Explorer 呈现的层次结构中唯一标识任何对象的方法。Shell 中的每个对象,无论它是否是文件系统的一部分,都有一个 PIDL。PIDL 的确切结构取决于对象所在的位置,但除非您正在编写自己的命名空间扩展,否则您不必(也不应该)担心 PIDL 的内部结构。
为了我们的目的,我们可以使用shell API SHGetPathFromIDList()
将PIDL转换为常规路径。如果目标文件夹不是文件系统中的目录(例如,“控制面板”),SHGetPathFromIDList()
将失败,我们就可以退出。
if ( !SHGetPathFromIDList(pidlFolder, m_szFolderDroppedIn) ) return E_FAIL;
接下来,我们必须检查目标目录是否在NTFS卷上。我们获取路径的根组件(例如,E:\
),并获取该卷的信息。如果文件系统不是NTFS,我们无法创建任何链接,因此我们可以返回。
lstrcpy ( szRoot, m_szFolderDroppedIn ); PathStripToRoot ( szRoot ); if ( !GetVolumeInformation ( szRoot, NULL, 0, NULL, NULL, NULL, szFileSystemName, 256 )) { // Couldn't determine file system type. return E_FAIL; } if ( 0 != lstrcmpi ( szFileSystemName, _T("ntfs") )) { // The file system isn't NTFS, so it doesn't support hard links. return E_FAIL; }
接下来,我们从数据对象获取一个HDROP
句柄,我们将使用它来枚举被拖放的文件。这与第三部分中的方法类似,只是我们使用MFC类来访问数据。COleDataObject
为我们处理设置FORMATETC
和STGMEDIUM
结构。
hglobal = dataobj.GetGlobalData ( CF_HDROP ); if ( NULL == hglobal ) return E_INVALIDARG; hdrop = (HDROP) GlobalLock ( hglobal ); if ( NULL == hdrop ) return E_INVALIDARG;
然后我们使用HDROP
句柄枚举拖放的文件。对于每个文件,我们检查它是否是目录。目录不能被链接,所以如果我们找到一个目录,我们可以返回。
uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );
for ( uFile = 0; uFile < uNumFiles; uFile++ )
{
if ( DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ) )
{
if ( PathIsDirectory ( szFile ) )
{
// We found a directory! Bail out.
m_lsDroppedFiles.RemoveAll();
break;
}
我们还必须检查拖放的文件是否与目标目录位于同一卷上。我所做的是比较每个文件和目标目录的根组件,如果它们不同则返回。然而,这不是一个完整的解决方案,因为在NTFS卷上,您可以在另一个卷的中间挂载一个卷。例如,您可以有一个C:卷,并将另一个卷挂载为C:\dev。此代码不会拒绝从C:\dev到C:上其他位置创建链接的尝试。
以下是比较根组件的检查:
if ( !PathIsSameRoot(szFile, m_szFolderDroppedIn) )
{
// Dropped files came from a different volume - bail out.
m_lsDroppedFiles.RemoveAll();
break;
}
如果文件通过两项检查,我们会将其添加到m_lsDroppedFiles
中,这是一个CStringList
(CString
的链表)。
// Add the file to our list of dropped files. m_lsDroppedFiles.AddTail ( szFile ); } } // end for
在for循环之后,我们释放资源并返回到Explorer。如果字符串列表包含任何文件名,我们返回S_OK
表示我们需要修改上下文菜单。否则,我们返回E_FAIL
,这样就不会再次为这个拖放事件调用我们。
GlobalUnlock ( hglobal ); return (m_lsDroppedFiles.GetCount() > 0) ? S_OK : E_FAIL; }
修改上下文菜单
像其他上下文菜单扩展一样,拖放处理程序实现IContextMenu
接口,通过该接口与上下文菜单交互。要将IContextMenu
添加到我们的扩展中,请再次打开HardLinkShlExt.h并添加粗体所示的行:
class CHardLinkShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CHardLinkShlExt, &CLSID_HardLinkShlExt>,
public IShellExtInit,
public IContextMenu
{
BEGIN_COM_MAP(CHardLinkShlExt)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
public:
// IContextMenu
STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT)
{ return E_NOTIMPL; }
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
请注意,我们不需要在GetCommandString()
中编写任何代码,因为在拖放处理程序中不会调用该方法。
Explorer 调用我们的 QueryContextMenu()
函数,让我们修改上下文菜单。这里没有什么你以前没见过;我们只是添加一个菜单项并设置其位图。
HRESULT CHardLinkShlExt::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags ) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC // If the flags include CMF_DEFAULTONLY then we shouldn't do anything. if ( uFlags & CMF_DEFAULTONLY ) return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 ); // Add the hard link menu item. InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd, _T("Create hard link(s) here") ); if ( NULL != m_bitmap.GetSafeHandle() ) { SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, (HBITMAP) m_bitmap.GetSafeHandle(), NULL ); } // Return 1 to tell the shell that we added 1 top-level menu item. return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1); }
这是新的菜单项的样子:
创建链接
当用户点击我们的菜单项时,Explorer 会调用 InvokeCommand()
。我们将为所有被拖放的文件创建链接。链接的名称将是 "Hard link to <filename>"
,如果该名称已被占用,则为 "Hard link (2) to <filename>"
。数字将增加到 99,这是一个任意限制。
首先,是局部变量和对lpVerb
参数的检查,该参数必须为0,因为我们只有一个菜单项。
HRESULT CHardLinkShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pInfo ) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC TCHAR szNewFilename[MAX_PATH+32]; CString sSrcFile; TCHAR szSrcFileTitle[MAX_PATH]; CString sMessage; UINT uLinkNum; POSITION pos; // Double-check that we're getting called for our own menu item - lpVerb // must be 0. if ( 0 != pInfo->lpVerb ) return E_INVALIDARG;
接下来,我们获取一个指向字符串列表开头的POSITION
值。POSITION
是一种不透明的数据类型,您不能直接使用它,而是将其传递给CStringList
类的其他方法。要获取列表头部的POSITION
,我们调用GetHeadPosition()
:
pos = m_lsDroppedFiles.GetHeadPosition(); ASSERT ( NULL != pos );
如果列表为空,pos
将为NULL,但列表不应该为空,所以我添加了一个ASSERT来检查这种情况。接下来是循环的开始,它将遍历列表中的文件名并为每个文件创建链接。
while ( NULL != pos ) { // Get the next source filename. sSrcFile = m_lsDroppedFiles.GetNext ( pos ); // Remove the path - this reduces "C:\xyz\foo\stuff.exe" to "stuff.exe" lstrcpy ( szSrcFileTitle, sSrcFile ); PathStripPath ( szSrcFileTitle ); // Make the filename for the hard link - we'll first try // "Hard link to stuff.exe" wsprintf ( szNewFilename, _T("%sHard link to %s"), m_szFolderDroppedIn, szSrcFileTitle );
GetNext()
返回pos
指示位置处的CString
,并将pos
递增以指向下一个字符串。如果pos
位于列表末尾,则pos
变为NULL(因此while循环将在此处结束)。
此时,szNewFilename
包含硬链接的完整路径。我们检查是否存在同名文件,如果存在,我们将尝试添加数字 2 到 99,寻找一个尚未使用的名称。我们还确保链接名称的长度(包括终止空字符)不超过 MAX_PATH 字符。
for ( uLinkNum = 2; PathFileExists(szNewFilename) && uLinkNum < 100; uLinkNum++ ) { // Try another filename for the link. wsprintf ( szNewFilename, _T("%sHard link (%u) to %s"), m_szFolderDroppedIn, uLinkNum, szSrcFileTitle ); // If the resulting filename is longer than MAX_PATH, show an // error message. if ( lstrlen(szNewFilename) >= MAX_PATH ) { sMessage.Format ( _T("Failed to make a link to %s.\nDo you want to continue making links?"), (LPCTSTR) sSrcFile ); if (IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO) ) break; else continue; } } // end for
消息框允许您根据需要中止整个操作。接下来,我们检查是否已达到99个链接的限制。同样,我们允许用户中止整个操作。
if ( 100 == uLinkNum )
{
sMessage.Format ( _T("Failed to make a link to %s.\nDo you want to continue making links?"),
(LPCTSTR) sSrcFile );
if (IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
MB_ICONQUESTION | MB_YESNO) )
break;
else
continue;
}
剩下的就是创建硬链接。为了清晰起见,我省略了错误处理代码。
CreateHardLink ( szNewFilename, sSrcFile, NULL );
} // end while
return S_OK;
}
硬链接在Explorer中看起来没有任何不同,它就像任何其他普通文件一样。但如果你修改其中一个副本,更改会反映在另一个副本中。
注册外壳扩展
注册拖放处理程序比其他上下文菜单扩展更简单。所有处理程序都注册在HKCR\Directory
键下,因为这是拖放操作发生的目录。然而,文档没有说明的是,仅仅在HKCR\Directory
下注册不足以处理所有情况。您还需要在HKCR\Folder
下注册以处理桌面上的拖放操作,并在HKCR\Drive
下注册以处理根目录中的拖放操作。
以下是处理上述三种情况的RGS脚本:
HKCR { NoRemove Directory { NoRemove shellex { NoRemove DragDropHandlers { ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}' } } } NoRemove Folder { NoRemove shellex { NoRemove DragDropHandlers { ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}' } } } NoRemove Drive { NoRemove shellex { NoRemove DragDropHandlers { ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}' } } } }
与我们之前的扩展一样,在基于NT的操作系统上,我们需要将我们的扩展添加到“批准”扩展列表中。执行此操作的代码位于示例项目中的DllRegisterServer()
和DllUnregisterServer()
函数中。
如果您没有Windows 2000/NTFS
您仍然可以在早期版本的Windows上构建和运行示例项目,或者如果您没有可用的NTFS卷。只需打开stdafx.h文件,并取消注释以下行:
// #define NOT_ON_WIN2K
这将使扩展跳过文件系统检查(因此它可以在任何系统上运行,而不仅仅是NTFS),并显示消息框而不是实际创建链接。
未完待续
在第五部分中,我们将看到一种新型的扩展,即属性表处理程序,它将页面添加到文件的属性对话框中。
版权和许可
本文是受版权保护的材料,©2000-2006 by Michael Dunn。我知道这不会阻止人们在网络上复制它,但我还是要说一下。如果您有兴趣翻译本文,请给我发电子邮件告诉我。我不预见会拒绝任何人翻译的许可,我只是想知道翻译情况,以便在此处发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布,以便代码可以造福每个人。(我没有将文章本身发布到公共领域,因为仅在CodeProject上提供文章有助于提高我自己的知名度和CodeProject网站。)如果您在自己的应用程序中使用演示代码,如果您能发邮件告诉我,我将不胜感激(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),但这不是必需的。在您的源代码中注明出处也值得赞赏,但不是必需的。
修订历史
2000年4月3日:文章首次发布。
2000 年 6 月 6 日:更新了某些内容。;)
2006年5月24日:更新以涵盖VC 7.1中的更改,并清理了代码片段。