您的位置:首页 > 运维架构 > Shell

自己动手写一个简单的Windows shell扩展程序

2012-05-19 20:03 801 查看
作者:朱金灿
来源:http://blog.csdn.net/clever101


关于什么叫Windowsshell扩展程序,这里不作介绍,不懂的同学请google之。


一.Shell程序编写

这里采用的开发环境为WindowsXP+sp3, VS 2005 + sp1 (应该支持VS 2005以上的VS版本,VC 6.0估计不支持)。

1.新建一个ATL项目,输入工程名:ImportShell,具体如下图:



2. 在应用程序设置中的服务器类型中选择:动态链接库(DLL),其它选项采用默认设置,具体如下图:


这样单击完成后就新建了ATL工程。
3.新建一个ATL简单对象(英文版的VS为ATLSimple Object),具体如下图:



4.输入一个简称:ImportShellExt,其它的VS会帮你自动填写,具体如下图:



新建CImportShellExt类需要新继承两个基类:IShellExtInit和IContextMenu。新加的接口函数主要有四个:
当我们的shell扩展被加载时, Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得一个 IShellExtInit 接口指针.
该接口仅有一个方法 Initialize(), 其函数原型为:

HRESULTIShellExtInit::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj,HKEY hProgID );

Explorer 使用该方法传递给我们各种各样的信息.
PidlFolder是用户所选择操作的文件所在的文件夹的 PIDL 变量. (一个 PIDL [指向ID列表的指针] 是一个数据结构,它唯一地标识了在Shell命名空间的任何对象, 一个Shell命名空间中的对象可以是也可以不是真实的文件系统中的对象.)
pDataObj 是一个IDataObject 接口指针,通过它我们可以获取用户所选择操作的文件名。
hProgID 是一个HKEY注册表键变量,可以用它获取我们的DLL的注册数据.

一旦 Explorer 初始化了扩展,它就会接着调用 IContextMenu 的方法让我们添加菜单项, 提供状态栏上的提示, 并响应执行用户的选择。

添加IContextMenu 方法的函数原型: public:
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO); 
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);

修改上下文菜单IContextMenu 有三个方法.
第一个是QueryContextMenu(), 它让我们可以修改上下文菜单. 其原型为:
HRESULT IContextMenu::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags );


hmenu 上下文菜单句柄.
uMenuIndex 是我们应该添加菜单项的起始位置.
uidFirstCmd 和 uidLastCmd 是我们可以使用的菜单命令ID值的范围.
uFlags 标识了Explorer 调用QueryContextMenu()的原因。

而返回值根据你所查阅的文档的不同而不同.
Dino Esposito 的书中说返回值是你所添加的菜单项的个数.
而 VC6.0所带的MSDN 又说它是我们添加的最后一个菜单项的命令ID加上1.
而最新的 MSDN 又说:
将返回值设为你为各菜单项分配的命令ID的最大差值,加上1.
例如, 假设 idCmdFirst 设为5,而你添加了三个菜单项 ,命令ID分别为 5, 7, 和8.
这时返回值就应该是: MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1).

我是一直按 Dino 的解释来做的, 而且工作得很好.
实际上, 他的方法与最新的 MSDN 是一致的, 只要你严格地使用 uidFirstCmd作为第一个菜单项的ID,再对接续的菜单项ID每次加1.

我们暂时的扩展仅加入一个菜单项,所以 QueryContextMenu() 非常简单:
HRESULT CImportShellExt::QueryContextMenu( HMENU hmenu,UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags )
{
	// 如果标志包含CMF_DEFAULTONLY 我们不作任何事情. 
	if ( uFlags & CMF_DEFAULTONLY ) 
	{ 
		return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 ); 
	} 

	InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("工程入库") ); 
	return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}

首先我们检查 uFlags.
你可以在 MSDN中找到所有标志的解释, 但对于上下文菜单扩展而言, 只有一个值是重要的: CMF_DEFAULTONLY.
该标志告诉Shell命名空间扩展保留默认的菜单项,这时我们的Shell扩展就不应该加入任何定制的菜单项,这也是为什么此时我们要返回 0 的原因.
如果该标志没有被设置, 我们就可以修改菜单了 (使用 hmenu 句柄), 并返回 1 告诉Shell我们添加了一个菜单项。

下一个要被调用的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 指向一个由Shell拥有的缓冲区,我们将把帮助字符串拷贝进该缓冲区.
cchMax 是该缓冲区的大小.
返回值是S_OK 或 E_FAIL.
GetCommandString() 也可以被调用以获取菜单项的动作("verb") .
verb 是个语言无关性字符串,它标识一个可以加于文件对象的操作。
ShellExecute()的文档中有详细的解释, 而有关verb的内容足以再写一篇文章, 简单的解释是:verb 可以直接列在注册表中(如"open" 和 "print"等字符串), 也可以由上下文菜单扩展创建. 这样就可以通过调用ShellExecute()执行实现在Shell扩展中的代码.

不管怎样, 我说了这多只是为了解释清楚GetCommandString() 的作用.
如果 Explorer 要求一个帮助字符串,我们就提供给它. 如果 Explorer 要求一个verb, 我们就忽略它. 这就是 uFlags 参数的作用.
如果 uFlags 设置了GCS_HELPTEXT 位,则 Explorer 是在要求帮助字符串. 而且如果 GCS_UNICODE 被设置,我们就必须返回一个Unicode字符串.

我们的 GetCommandString() 如下:

#include <atlconv.h>
// 为使用 ATL 字符串转换宏而包含的头文件 
             
HRESULT CImportShellExt::GetCommandString( UINT idCmd, UINT uFlags,UINT* pwReserved, LPSTR pszName, UINT cchMax ) 
{
	USES_CONVERSION; 
	//检查idCmd, 它必须是,因为我们仅有一个添加的菜单项. 
	if ( 0 != idCmd ) 
	   return E_INVALIDARG; 

	// 如果Explorer 要求帮助字符串,就将它拷贝到提供的缓冲区中. 
	if ( uFlags & GCS_HELPTEXT ) 
	{
		LPCTSTR szText = _T("统计文件夹中的文件个数");              
		if ( uFlags & GCS_UNICODE )
		{ 
			// 我们需要将pszName 转化为一个Unicode 字符串, 接着使用Unicode字符串拷贝API. 
			lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax ); 
		} 
		else
		{ 
			// 使用ANSI 字符串拷贝API 来返回帮助字符串. 
			lstrcpynA ( pszName, T2CA(szText), cchMax ); 
		} 
		return S_OK;
	} 
	return E_INVALIDARG; 
}


这里没有什么特别的代码; 我用了硬编码的字符串并把它转换为相应的字符集.
如果你从未使用过ATL字符串转化宏,你一定要学一下,因为当你传递Unicode字符串到COM和OLE函数时,使用转化宏会很有帮助的.
我在上面的代码中使用了T2CW 和 T2CA 将TCHAR 字符串分别转化为Unicode 和 ANSI字符串.
函数开头处的USES_CONVERSION 宏其实声明了一个将被转化宏使用的局部变量.

要注意的一个问题是: lstrcpyn() 保证了目标字符串将以null为结束符.
这与C运行时(CRT)函 数strncpy()不同.当要拷贝的源字符串的长度大于或等于cchMax 时 strncpy()不会添加一个 null 结束符.
我建议总使用lstrcpyn(), 这样你就不必在每一个strncpy()后加入检查保证字符 串以null为结束符的代码.
IContextMenu 接口的最后一个方法是InvokeCommand(). 当用户点击我们添加的菜单项时该方法将被调用. 其函数原型是:
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );

CMINVOKECOMMANDINFO 结构带有大量的信息, 但我们只关心 lpVerb 和 hwnd 这两个成员.
lpVerb参数有两个作用 – 它或是可被激发的verb(动作)名, 或是被点击的菜单项的索引值.
hwnd 是用户激活我们的菜单扩展时所在的浏览器窗口的句柄.

因为我们只有一个扩展的菜单项, 我们只要检查lpVerb 参数,如果其值为0, 我们可以认定我们的菜单项被点击了。我能想到的最简单的代码就是弹出一个信息框, 这里的代码也就做了这么多. 信息框显示所选的文件夹的名字。具体代码如下:
HRESULT CImportShellExt::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("选中的文件夹为%s"),m_szFile);
			MessageBox ( pCmdInfo->hwnd, szMsg, _T("信息"),
				MB_ICONINFORMATION );
			return S_OK;
		}
		break;
	default:
		return E_INVALIDARG;
		break;
	}
}

这时可能你会问:操作系统是如何知道我们要插入这个菜单的?这里涉及到一个COM组件的注册问题。所谓COM组件的注册,简单来说是将COM组件的相关信息写进注册表,然后操作系统通过读取注册表的相关信息来加载COM组件。Shell程序的注册分为两步:
第一步在Win NT/Win 2000上确保你的Shell扩展能被没有管理员权限的用户调用,需要在注册表HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ShellExtensions\Approved添加我们的程序信息。这个需要在工程中的DllRegisterServer函数(注册函数)和DllUnregisterServer函数(反注册函数)。代码如下:
// DllRegisterServer - 将项添加到系统注册表
STDAPI DllRegisterServer(void)
{
    // 注册对象、类型库和类型库中的所有接口
	if ( 0 == (GetVersion() & 0x80000000UL) )
	{
		CRegKey reg;
		LONG    lRet;

		lRet = reg.Open ( HKEY_LOCAL_MACHINE,
			_T("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"),
			KEY_SET_VALUE );

		if ( ERROR_SUCCESS != lRet )
			return E_ACCESSDENIED;

		lRet = reg.SetValue ( _T("ImportShell extension"), 
			_T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );

		if ( ERROR_SUCCESS != lRet )
			return E_ACCESSDENIED;
	}

    HRESULT hr = _AtlModule.DllRegisterServer();
	return hr;
}

// DllUnregisterServer - 将项从系统注册表中移除
STDAPI DllUnregisterServer(void)
{
	if ( 0 == (GetVersion() & 0x80000000UL) )
	{
		CRegKey reg;
		LONG    lRet;

		lRet = reg.Open ( HKEY_LOCAL_MACHINE,
			_T("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"),
			KEY_SET_VALUE );

		if ( ERROR_SUCCESS == lRet )
		{
			lRet = reg.DeleteValue ( _T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );
		}
	}

	HRESULT hr = _AtlModule.DllUnregisterServer();
	return hr;
}

这里的一个问题是reg.SetValue ( _T("ImportShell extension"),
_T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );
中的键名和键值是如何来的。实际上当你新建COM简单对象后,就会自动生成一个ImportShellExt.rgs文件,打开这个ImportShellExt.rgs文件,就会有如下的文件:
ImportShell.ImportShellExt.1 = s 'ImportShellExt Class'
{
CLSID = s '{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
ImportShell.ImportShellExt = s 'ImportShellExt Class'
{
CLSID = s '{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
CurVer = s 'ImportShell.ImportShellExt.1'
}
这个键名一般取自程序名+ extension,如ImportShell extension,键值则来自它的guid的字符串形式: {06001B8E-8858-4CEE-8E91-60E12A6C81A7}。

第二步则涉及到该Shell程序所操作的文件类型。比如我们要求它在选中文件夹才弹出我们这个右键菜单。这时就需要在ImportShellExt.rgs文件添加一些信息:
NoRemove Folder
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ImportShellExt = s'{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
}
}

上面这个其实很好理解的:每一行代表一个注册表键, "HKCR"是HKEY_CLASSES_ROOT 的缩写.
NoRemove 关键字表示当该COM服务器注销时该键 不用被删除.
最后一行有些复杂. ForceRemove 关键字表示如果该键已存在, 那么在新键添加之前该键先应被删除. 这行脚本的余下部分指定一个字符串,它将被存为ImportShell键的默认值。

如果你要操作txt文件,可以添加这样的信息:
NoRemove .txt
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ImportShellExt = s'{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
}
}

如果要操作任意类型的文件,则是:
NoRemove *
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove ImportShellExt = s'{06001B8E-8858-4CEE-8E91-60E12A6C81A7}'
}
}
}


二.Shell程序调试

在Win NT/2000上, 你可以找到如下键:
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer
并创建一个名为DesktopProcess的DWORD值 1. 这会使桌面和任务栏运行在同一个进程中, 而其他每一个Explorer 窗口都运行在它自己的每一个进程内. 也就是说,你可以在单个的Explorer窗口内进行调试, 而后只要你关闭该窗口,你的DLL就会被马上卸载, 这就避免了因为DLL正被Windows使用而无法替换更新. 而如果不幸出现这种情况,你就不得不注销登录后再重新登录进Windows从而强制卸载使用中的Shell扩展DLL。

按F5开始,这时会弹出一个对话框,这时请输入exploer.exe的路径,如下图:



这时一般会出现一个警告框,按是不予理会,如下图:




接着是打开一个我的文档的窗口,如下图:



这时就可以在代码中设置断点调试了。

三.Shell程序的部署
Shell程序的部署很简单,就是在生成的dll的目录下新建两个批处理文件:
install.bat ——shell程序的安装脚本,内容为:
regsvr32.exe ImportShell.dll
uninstall.bat ——shell程序的卸载脚本,内容为:
regsvr32.exe /u ImportShell.dll

运行这两个批处理文件就能安装或卸载shell程序。


四.遇到问题及解决办法

链接器工具错误 LINK : fatal error LNK1168: cannot open..\outdir\Debug\ImportShell.dll for writing。

在改变注册com对象的guid会出现该问题。解决办法是打开任务管理器,杀死所有explorer.exe,然后新建一个explorer进程。


参考文献:

《Windows Shell扩展编程完全指南》,作者:Michael Dunn。

文中参考工程源码下载地址:CSDN下载

如果你觉得我的博客对你有帮助,请在下面网址中博客之星评选活动投我一票:

http://vote.blog.csdn.net/item/blogstar/clever101(单击候选人介绍下面的投他一票那个按钮)

参与投票有机会获奖:

最佳贡献奖:通过微博分享活动就有机会获得30元充值卡一张(每周抽选5名)
幸运奖:凡参与投票用户就有机会获得精美小礼品一份。(每周抽选5名)
积极参与奖:所有参与投票并符合条件的用户均可获得20个下载积分。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: