第一部分包括了对外壳扩展的概要的介绍,并提供了一个上下文菜单扩展的例程来使你对以后的部分中充满兴趣。
什么是外壳扩展呢?这有两部分,外壳和扩展(extension)。外壳指的是资源管理器(Explorer),而扩展是指当一个预订的事件(如:右键单击一个.doc文档)发生时,被资源管理器调用的你写的代码。所以以个外壳扩展是一个向资源管理器添加特色的COM对象。
一个外壳扩展是一个进程中服务器,它实现了一些与资源管理器通信的借口。而在我看来,ATL是快速实现一个扩展并使它运行的最简单的方法,因为你不用为一遍又一遍的写QueryInterface()和AddRef()而大伤脑筋。而且在Windows NT/2000下调试扩展也变得更为容易。
有很多种的扩展,每种扩展在不同的事件发生时被调用。下面是一些比较通用的类型和它们被调用的情况:
类型
什么时候被调用
可以做什么
上下文菜单
用户在文件或目录右键单击时。在外壳扩展4.71版本以上,在目录窗口的背景上右键单击也将被调用。
向上下文菜单添加项目。
属性单
文件的属性单被显示时。
向属性单添加一个属性页。
拖扔
用户右键拖动项目并把它扔在一个目录窗口活着桌面上时。
添加项目至上下文菜单。
扔
用户拖一个项目并把它扔到一个文件上时。
任何你想做的事。
查询信息(外壳版本4.71+)
用户鼠标在一个文件或像我的电脑一样的其他外壳对象上悬停时。
返回一个资源管理器在工具条提示上的字符串。
到现在为止你可能为什么一个扩展看起来想在资源管理器里。如果你安装了WinZip(有谁没有吗?),它就包括了许多种的外壳扩展,其中一个就是上下文句柄。下面世WinZip 8 为了压缩文件添加到上下文菜单的截图:
WinZip包含了添加菜单项目的代码,并提供敏感帮助(显示在资源管理器状态条的文本),并在用户选择WinZip命令之一时起作用。
WinZip还包含了拖和扔的句柄。这个类型和上下文菜单扩展非常类似,但是它仅仅在用户通过鼠标右键拖动一个文件时才被调用。下面是WinZip的拖扔句柄如何添加上下文菜单:
还有很多种其他类型(微软一直往新版本的Windows里添加更多内容)。到现在,我们已经看到了上下文菜单扩展,因为它非常容易编写,我们将很容易的看到它的结果(很快就能满意)。
在我们开始编码以前,有一些提示,它将使我们做起来更加容易。当你促成一个外壳扩展被资源管理器调用的时候,它将在内存中呆上一小会儿,从而使它不能立即被重建(rebuild)。为了使资源管理器更加频繁的卸载这些扩展,创建这个注册表键:
HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL
并把它的默认值设为”1”。在Windows 9x系列中,这是最好的方法。在NT/2000,到如下的键:
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer
创建一个叫做DesktopProcess的双字节值,使它的值为1。这使得桌面和任务栏运行在一个进程中,后发的资源管理器运行在它自己的进程里。这就意味着你可以使用一个单独的资源管理器窗口来调试,并且当你关掉它的时候,你的DLL也会自动的被卸载,避免了该文件仍然在使用得问题。要使得你的注册表修改生效的话,你必须注销并且重新登录。
我将稍晚一些解释如何在Win 9x下进行调试。
开始一个上下文菜单扩展 – 它能做什么?让我们开始简单的做一个扩展,它仅仅弹出一个消息框表示它已经在工作了。我们将对扩展名为.txt的文件设置一个钩子,这样当用户右键单击一个文本文件的时候,我们的扩展就能被调用了。
使用AppWizard 开始好了,现在是我们开始的时候了。那是什么?我还没有告诉你如何使用神秘的外壳扩展接口?不要担心,我将在接下来的过程中为你解释。我发现如果一个概念被解释,有一个例子更容易明白,通过例子代码你很快就能理解。我将会先解释任何东西,然后给出代码,但是我发现还是不容易吸收。总之,启动你的MSVC吧,我们要开始了。
运行AppWizard ,做一个新的ATL COM wizard app。我们叫它SimpleExt 。在向导中保持所有默认选项,单击完成。我们现在就有了一个空的将会生成DLL的ATL项目,但是我们必须添加自己的外壳扩展COM对象。在ClassView树中,右键单击SimpleExt classes 项,选择New ATL Object。
在ATL Object 向导,第一面板已经选择了Simple Object ,只要单击下一步就行了。在第二面板中,在Short Name 编辑控件中输入SimpleShlExt ,然后单击确定(面板中的其它的编辑框将会自动完成)。这就创建了一个类名为CSimpleShlExt 的类,它包含了实现一个COM对象的基本代码。我们将向这个类添加我们的代码。
初始化接口当我们的外壳扩展被装载的时候,资源管理器调用我们的QueryInterface()函数获取一个指向IShellExtInit 接口的指针。这个接口只有一个方法,Initialize(),它的原型如下:
HRESULT IShellExtInit::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID );
资源管理器使用这个方法给我们不同的信息。pidlFolder 是包含有正在被作用的文件的文件夹的PIDL(PIDL[pointer to an ID list]是唯一标志外壳中任一对象(无论是否是文件系统对象)的数据结构。pDataObj 是一个IDataObject 接口指针,通过它我们可以获得被作用的文件的文件名。hProgID 是一个打开的HKEY ,通过它我们可以访问包含有我们的DLL注册数据的注册表键。在这个简单的扩展中,我们只需要用到pDataObj参数。
添加这个接口方法到我们的COM 对象中,先打开SimpleShlExt.h 文件,并添加如下用红色书写的代码行:
#include <shlobj.h>
#include <comdef.h>
class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
public IDispatchImpl<ISimpleShlExt, &IID_ISimpleShlExt, &LIBID_SIMPLEEXTLib>,
public IShellExtInit
{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
这个COM_MAP是说明了ATL如何实现它的QueryInterface()。这个列表告诉ATL其他使用QueryInterface()的程序可以从我们这儿获得什么。
接着,在类的声明当中,添加Initialize()函数。此外,我们还需要一个保存文件名的变量:
protected:
TCHAR m_szFile [MAX_PATH];
public:
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
接下来,在SimpleShlExt.cpp 文件中,添加该函数的定义:
HRESULT CSimpleShlExt::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID )
我们所要做的就是获得被右键单击的文件的文件名,并把它显示在一个消息框中。如果有很多个文件被选中,你可以通过pDataObj 接口指针访问它们。但是为了保持该例子的简单性,我只要获得第一个文件的文件名。
文件名被保存为与你使用WS_EX_ACCEPTFILES样式拖和扔一个文件到窗口时用到的一个相同的格式。那就意味着,我们获得文件名使用了相同的API: DragQueryFile()。我们通过获得包含在IDataObject 中的数据的句柄开始这个函数:
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP hDrop;
// Look for CF_HDROP data in the data object.
if ( FAILED( pDataObj->GetData ( &fmt, &stg )))
{
// Nope! Return an "invalid argument" error back to Explorer.
return E_INVALIDARG;
}
// Get a pointer to the actual data.
hDrop = (HDROP) GlobalLock ( stg.hGlobal );
// Make sure it worked.
if ( NULL == hDrop )
{
return E_INVALIDARG;
}
要注意,错误检查是极其重要的,尤其是指针。因为我们的扩展运行在资源管理器的进程空间当中,如果我们的程序毁坏的话,同样会让资源管理器也毁坏的。在Win 9x下,这可能就意味着重新启动。
现在,我们有了一个HDROP 句柄,我们可以获得我们需要的文件名了。
// Sanity check – make sure there is at least one filename.
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
if ( 0 == uNumFiles )
{
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
HRESULT hr = S_OK;
// Get the name of the first file and store it in our member variable m_szFile.
if ( 0 == DragQueryFile ( hDrop, 0, m_szFile, MAX_PATH ))
{
hr = E_INVALIDARG;
}
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return hr;
}
如果我们返回E_INVALIDAR,在右键单击事件发生时,资源管理器将不会再调用我们的扩展。如果我们返回S_OK ,那么资源管理器将会再次调用QueryInterface()来获得我们将要添加的另一个接口指针:IContextMenu 。
和上下文菜单交互的接口一旦资源管理器初始化了我们的扩展,它将会调用IContextMenu 的方法让我们添加菜单项目、敏感帮助并完成用户的选择。
向我们的扩展中添加IContextMenu 接口和添加IShellExtInit相类似。打开SimpleShlExt.h并添加一下红颜色的代码:
class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
public IDispatchImpl<ISimpleShlExt, &IID_ISimpleShlExt, &LIBID_SIMPLEEXTLib>,
public IShellExtInit,
public IContextMenu
{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
接着添加IContextMenu 方法的原型:
public:
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT); 更改上下文菜单
IContextMenu 有3个方法。第一个,QueryContextMenu(),让我们更改菜单。它的原型为:
HRESULT IContextMenu::QueryContextMenu (
HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags );
hmenu 是上下文菜单的句柄。uMenuIndex 是我们开始添加我们的菜单项目的开始位置。 uidFirstCmd 和 uidLastCmd 是我们可以给菜单项目使用的命令ID值的范围。uFlags 指出为什么资源管理器正在调用QueryContextMenu(),这我们将在以后看到。
有关它的返回值你将会得到不同的答案,如果你问不同的人的话。Dino Esposito 的书上说它使被QueryContextMenu()添加的菜单项目的号码。MSDN上关于VC 6部分说它是最后一个被添加的菜单项目的命令ID加上1。最新的MSDN文档有如下说明:
设置代码的值[由HRESULT返回的]为被分配的最大的命令ID偏移加上1。例如,假定idCmdFirst被设置为5,你添加了3个菜单项目分别使用命令ID为5、7和8。它的返回值将是MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)。
到我现在所写的所有代码中,我接受了Dino的解释,这样工作的很好。事实上,他的制作返回值的方法和在线MSDN的方法是相同的,在你使用uidFirstCmd开始添加你的菜单项目时开始计数,每添加一个增加1。
我们的简单的扩展将仅仅添加一个菜单项目,所以QueryContextMenu()函数相当简单:
HRESULT CSimpleShlExt::QueryContextMenu (
HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags )
{
// If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
if ( uFlags & CMF_DEFAULTONLY )
{
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
}
InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("SimpleShlExt Test Item") );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
我们首先要做的就是检查uFlags。你可以在MSDN查询所有的标志列表,但是对于上下文菜单扩展来说,只有一样是重要的:CMF_DEFAULTONLY。这个标志告诉名字空间扩展仅仅添加默认菜单项目。如果这个标志在的话,外壳扩展将不会添加任何菜单项目。这就是为什么当CMF_DEFAULTONLY存在的时候我们立即返回0的原因。如果该标志不存在,我们更改菜单(使用hmenu 句柄),然后返回1告诉外壳我们添加了一个菜单项目。
在状态条显示敏感帮助IContextMenu 中下一个可以调用的方法是GetCommandString()。如果用户在资源管理器窗口中右键单击了一个文本文件的时候,或者选中一个文本文件,然后单击“文件”菜单,状态条上将显示敏感帮助。我们的GetCommandString() 函数将会返回一个让资源管理器显示得字符串。
GetCommandString()函数原型如下:
HRESULT IContextMenu::GetCommandString (
UINT idCmd,
UINT uFlags,
UINT *pwReserved,
LPSTR pszName,
UINT cchMax );
idCmd 是一个基于0的指明哪个菜单项目被选中的数。因为我们仅仅添加了一个菜单项,idCmd 将总是为0。但是如果我们添加了,我是说,3个的话,idCmd 将会是0,1或者2。uFlags是另一个标志组。我将会在后面进行描述。我们可以忽略pwReserved。pszName 是一个指向一个被外壳所拥有的缓存的指针,该缓存保存被显示的帮助字符串。cchMax 是缓存的大小。返回值是HRESULT常量,例如S_OK或E_FAIL。
GetCommandString()同样能被用来获得菜单项的“动词”。“动词”是一个标志作用于文件的动作的字符串,它是独立于语言的。有关ShellExecute()的文档作了更多的说明,有关“动词”的主题更适合在另一篇文章说明,这儿简要说明的是列在注册表中的动词(比如说”open”和”print”),或者那些有上下文菜单扩展动态创建的“动词”。这使得在外壳扩展中实现的行为可以被ShellExecute()调用。
总之,我提及所有这些的原因是我们不得不确定为什么GetCommandString()被调用。如果资源管理器需要一个敏感帮助字符串的时候,我们就提供。如果资源管理器请求一个“动词”的话,我们将忽略它。这是uFlags起作用的地方。如果uFlags 的GCS_HELPTEXT 的位被设置的话,那么资源管理器将请求敏感帮助。附加的,如果GCS_UNICODE位被设置,我们必须返回一个Unicode字符串。
我们的GetCommandString() 的代码看起来应该像下面这样:
#include <atlconv.h> // for ATL string conversion macros
HRESULT CSimpleShlExt::GetCommandString (
UINT idCmd,
UINT uFlags,
UINT* pwReserved,
LPSTR pszName,
UINT cchMax )
{
USES_CONVERSION;
// Check idCmd, it must be 0 since we have only one menu item.
if ( 0 != idCmd )
return E_INVALIDARG;
// If Explorer is asking for a help string, copy our string into the
// supplied buffer.
if ( uFlags & GCS_HELPTEXT )
{
LPCTSTR szText = _T("This is the simple shell extension's help");
if ( uFlags & GCS_UNICODE )
{
// We need to cast pszName to a Unicode string, and then use the
// Unicode string copy API.
lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
}
else
{
// Use the ANSI string copy API to return the help string.
lstrcpynA ( pszName, T2CA(szText), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}
没什么奇特的;我只是把字符串编码并且把它转换为合适的字符集。如果你以前从来都没有使用过ATL变换宏,你干脆先看看它们,因为这将我使更容易理解传递一个Unicode字符串到COM方法和OLE函数中。在上面的代码中,我使用了T2CW和T2CA 分别将TCHAR字符串转换为Unicode和ANSI。在函数头部的USES_CONVERSION 宏声明了一个变换宏使用的局部变量。
一个需要注意的重要事项是lstrcpyn()API函数保证了目标字符串是以null结束的。这是它和CRT函数strncpy()的不同之处。如果源字符串的长度大于或等于cchMax时, strncpy() 并不添加结束符null。我建议你总是使用lstrcpyn(),这样你就不用不得不在strncpy()之后添加检查来保证字符串是null结束的。
执行用户的选择最后一个IContextMenu方法是InvokeCommand()。这个方法将在用户单击我们添加的那个菜单项目时被调用。它的原型如下:
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );
CMINVOKECOMMANDINFO 结构里有很多的信息,但是根据我们现在的意图,我们只需要关心lpVerb和hwnd 。lpVerb 有双重的任务——它既可以是被调用的“动词”的名称,也可以是一个用以告诉我们哪个菜单项被选中地索引。hwnd 是资源管理器窗口的句柄,在那儿,用户调用了我们的扩展。
我们检查lpVerb ,因为我们只添加了一个菜单项,所以如果它为0,则我们的菜单被点击了。我能想到的最简单的事就是弹出一个消息框,所以我们就这么做。这个消息框显示了选中的文件的文件名,证明它的确是在工作。
HRESULT CSimpleShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
{
// If lpVerb really points to a string, ignore this function call and bail out.
if ( 0 != HIWORD( pCmdInfo->lpVerb ))
return E_INVALIDARG;
// Get the command index - the only valid one is 0.
switch ( LOWORD( pCmdInfo->lpVerb ))
{
case 0:
{
TCHAR szMsg [MAX_PATH + 32];
wsprintf ( szMsg, _T("The selected file was:\n\n%s"), m_szFile );
MessageBox ( pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"),
MB_ICONINFORMATION );
return S_OK;
}
break;
default:
return E_INVALIDARG;
break;
}
} 注册外壳扩展
到现在为止,我们已经实现了我们所有的COM接口。但是……如何使资源管理器使用我们的扩展呢?ATL自动生成了注册我们的DLL为一个COM服务器的代码,但是它仅仅是让其它程序来使用我们的DLL。为了告诉资源管理器我们的扩展存在,我们必须在保持文本文件的注册表键下注册它:
HKEY_CLASSES_ROOT\txtfile
在那个键下面,一个叫做ShellEx 的键保存了一个对于文本文件将被调用的外壳扩展列表。在ShellEx下,ContextMenuHandlers 键保存了一个上下文菜单扩展的列表。每一个扩展在ContextMenuHandlers下创建一个字键,并把他的默认值设置为它的GUID。所以,为我们的扩展,我们创建如下键:
HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt
并把它的默认值设置为我们的GUID:
"{5E2121EE-0300-11D4-8D3B-444553540000}"。
然而,你不用自己做这件事。如果你在FileView页查看你得文件列表时,你会发现SimpleShlExt.rgs。这是一个由ATL解析的文本文件,它告诉ATL当这个服务器被注册时该添加什么键,当被反注册时又该删除什么键。下面我们指定了要添加的注册表入口:
HKCR
{
NoRemove txtfile
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove SimpleShlExt = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
}
}
}
}
它以"HKCR"——HKEY_CLASSES_ROOT的缩写——开头,每一行是注册表键名称。关键词NoRemove 意味着当该服务器被反注册时该键不能被删除。最后一行有点复杂。关键词ForceRemove 意思是如果该键存在,那么在该键被写之前先删除它。这一行剩下的部分指定了一个将被保存在SimpleShlExt键的默认值中的字符串(那就是”s”的意思)。
在这儿,我需要说明一点。我们注册扩展时的键是HKCR\txtfile。然而,这个名称"txtfile" 并不是一个永久的或预先知道的。如果你查看一下HKCR\.txt ,那个键的默认值是这个名称被保存的地方。这就两个侧面效果:
我们将不能可靠的使用RGS脚本,因为"txtfile"可能不是正确的键名。 其他的一些文本编辑器可能被安装,它们同.TXT文件相关联。如果它们改变了 HKCR\.txt键的默认值,所有存在的外壳扩展都将会停止工作。看起来,这的确是我设计的缺陷。我想微软也在考虑同样的事,因为最近创建的扩展,像QueryInfo扩展,是在.txt键下注册的。
好了,说明到这儿。有一个最终的注册细节。在Win NT/2000下,我们必须自己将我们的扩展方到一个“被认可的”扩展列表当中。如果我们不这么做的话,那些非管理员用户将不会壮在我们的扩展。这个列表被保存在:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved
在这个键下,我们创建一个字符串值它的名称是我们的GUID。字符串的内容可以是任何东西。做这些事情的代码在我们的DllRegisterServer()和DllUnregisterServer()函数当中。我并不想把这些呆马列在这儿,因为这只是简单的注册表访问。你可以从本文的例子项目当中找到它们。
调试外壳扩展最终,你写成了这个相当不容易的扩展,然后你将会调试它。打开你的项目设置(Project->Settings),到Debug栏,在"Executable for debug session"编辑框中输入资源管理器的全路径,例如:"C:\windows\explorer.exe"。如果你使用的是NT或2000,而且你已经设置过了DesktopProcess 注册表项,那么在你按F5开始调试的时候,会有一个新的资源管理器窗口打开。只要你在那个窗口工作,以后重建DLL时你将不会有问题,因为当你关掉窗口时,你的扩展也被卸载了。
在Windows 9x下,恐怕你不得不在调试之前关闭你的外壳。单击“开始”->“关闭系统”。按住Ctrl+Alt+Shift然后点击“取消”。这将关闭资源管理器,然后你看见任务栏消失了。切换到MSVC然后按F5开始调试。按Shift+F5关闭资源管理器停止调试。当你做完调试的时候,你可以运行Explorer重新正常启动你的外壳。
它看起来是什么样的?下面是我们添加的项目看起来的样子:
这就是我们的菜单!
下面是有敏感帮助时资源管理器的状态栏的样子:
而下面是消息框的样子,它显示了被选中的文件的文件名:
本例程代码下载地址(11K):http://www.codeproject.com/shell/ShellExtGuide1/ShellExtGuide1_demo.zip
下一部分……接着的第二部分,一个新的上下文菜单扩展将会告诉你如何同时对多个文件进行操作。
你可以从下面的网址获得这个和其他文章的最新版本:http://home.inreach.com/mdunn/code/
关于翻译:
这是我第一次翻译文章,文章来自著名的http://www.codeproject.com/,翻译之前我看了csdn的开发文档,发现还是空白,所以就像把它翻译了,也许有对它感兴趣的人。文章总共有9个部分。我没有太多的时间,只翻译了第一部分,也许能起到抛砖引玉的作用,让那些对外壳扩展不了解的人入个门,入了门的多个参考。更多的文章大家可以从http://www.codeproject.com/shell/ 找到。例子代码,原文也可以从那儿找到。我的email是[email protected],头一次翻译,做得不好,任何意见、建议、鲜花、掌声、石头、带酒的啤酒瓶都将受到热烈欢迎……
本文地址:http://com.8s8s.com/it/it2991.htm