一个简单的Windows shell扩展程序

    关于什么叫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:

  1. // IContextMenu  
  2. STDMETHOD(GetCommandString)(UINTUINTUINT*, LPSTRUINT);  
  3. STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);   
  4. STDMETHOD(QueryContextMenu)(HMENUUINTUINTUINTUINT);    
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO); 
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);  

修改上下文菜单IContextMenu 有三个方法.

第一个是QueryContextMenu(), 它让我们可以修改上下文菜单. 其原型为:

  1. HRESULT IContextMenu::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags );   
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() 非常简单:

  1. HRESULT CImportShellExt::QueryContextMenu( HMENU hmenu,UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags )  
  2. {  
  3.     // 如果标志包含CMF_DEFAULTONLY 我们不作任何事情.   
  4.     if ( uFlags & CMF_DEFAULTONLY )   
  5.     {   
  6.         return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );   
  7.     }   
  8.   
  9.     InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("工程入库") );   
  10.     return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );  
  11. }  
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() 的原型是:

  1. HRESULT IContextMenu::GetCommandString ( UINT idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax );  
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_OKE_FAIL.

GetCommandString() 也可以被调用以获取菜单项的动作("verb") .
verb 是个语言无关性字符串,它标识一个可以加于文件对象的操作。
ShellExecute()的文档中有详细的解释, 而有关verb的内容足以再写一篇文章, 简单的解释是:verb 可以直接列在注册表中(如"open" 和 "print"等字符串), 也可以由上下文菜单扩展创建. 这样就可以通过调用ShellExecute()执行实现在Shell扩展中的代码.

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

我们的
GetCommandString() 如下:

  1. #include <atlconv.h>  
  2. // 为使用 ATL 字符串转换宏而包含的头文件   
  3.                
  4. HRESULT CImportShellExt::GetCommandString( UINT idCmd, UINT uFlags,UINT* pwReserved, LPSTR pszName, UINT cchMax )   
  5. {  
  6.     USES_CONVERSION;   
  7.     //检查idCmd, 它必须是,因为我们仅有一个添加的菜单项.   
  8.     if ( 0 != idCmd )   
  9.        return E_INVALIDARG;   
  10.   
  11.     // 如果Explorer 要求帮助字符串,就将它拷贝到提供的缓冲区中.   
  12.     if ( uFlags & GCS_HELPTEXT )   
  13.     {  
  14.         LPCTSTR szText = _T("统计文件夹中的文件个数");                
  15.         if ( uFlags & GCS_UNICODE )  
  16.         {   
  17.             // 我们需要将pszName 转化为一个Unicode 字符串, 接着使用Unicode字符串拷贝API.   
  18.             lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );   
  19.         }   
  20.         else  
  21.         {   
  22.             // 使用ANSI 字符串拷贝API 来返回帮助字符串.   
  23.             lstrcpynA ( pszName, T2CA(szText), cchMax );   
  24.         }   
  25.         return S_OK;  
  26.     }   
  27.     return E_INVALIDARG;   
  28. }  
#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(). 当用户点击我们添加的菜单项时该方法将被调用. 其函数原型是:

  1. HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );  
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );

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

        因为我们只有一个扩展的菜单项, 我们只要检查
lpVerb 参数,如果其值为0, 我们可以认定我们的菜单项被点击了。我能想到的最简单的代码就是弹出一个信息框, 这里的代码也就做了这么多. 信息框显示所选的文件夹的名字。具体代码如下:

  1. HRESULT CImportShellExt::InvokeCommand( LPCMINVOKECOMMANDINFO pCmdInfo )  
  2. {  
  3.     // If lpVerb really points to a string, ignore this function call and bail out.  
  4.     if ( 0 != HIWORD( pCmdInfo->lpVerb ) )  
  5.         return E_INVALIDARG;  
  6.   
  7.     // Get the command index - the only valid one is 0.  
  8.     switch ( LOWORD( pCmdInfo->lpVerb) )  
  9.     {  
  10.     case 0:  
  11.         {  
  12.             TCHAR szMsg [MAX_PATH + 32];  
  13.             wsprintf ( szMsg, _T("选中的文件夹为%s"),m_szFile);  
  14.             MessageBox ( pCmdInfo->hwnd, szMsg, _T("信息"),  
  15.                 MB_ICONINFORMATION );  
  16.             return S_OK;  
  17.         }  
  18.         break;  
  19.     default:  
  20.         return E_INVALIDARG;  
  21.         break;  
  22.     }  
  23. }   
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函数(反注册函数)。代码如下:

  1. // DllRegisterServer - 将项添加到系统注册表  
  2. STDAPI DllRegisterServer(void)  
  3. {  
  4.     // 注册对象、类型库和类型库中的所有接口  
  5.     if ( 0 == (GetVersion() & 0x80000000UL) )  
  6.     {  
  7.         CRegKey reg;  
  8.         LONG    lRet;  
  9.   
  10.         lRet = reg.Open ( HKEY_LOCAL_MACHINE,  
  11.             _T("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"),  
  12.             KEY_SET_VALUE );  
  13.   
  14.         if ( ERROR_SUCCESS != lRet )  
  15.             return E_ACCESSDENIED;  
  16.   
  17.         lRet = reg.SetValue ( _T("ImportShell extension"),   
  18.             _T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );  
  19.   
  20.         if ( ERROR_SUCCESS != lRet )  
  21.             return E_ACCESSDENIED;  
  22.     }  
  23.   
  24.     HRESULT hr = _AtlModule.DllRegisterServer();  
  25.     return hr;  
  26. }  
  27.   
  28.   
  29. // DllUnregisterServer - 将项从系统注册表中移除  
  30. STDAPI DllUnregisterServer(void)  
  31. {  
  32.     if ( 0 == (GetVersion() & 0x80000000UL) )  
  33.     {  
  34.         CRegKey reg;  
  35.         LONG    lRet;  
  36.   
  37.         lRet = reg.Open ( HKEY_LOCAL_MACHINE,  
  38.             _T("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"),  
  39.             KEY_SET_VALUE );  
  40.   
  41.         if ( ERROR_SUCCESS == lRet )  
  42.         {  
  43.             lRet = reg.DeleteValue ( _T("{06001B8E-8858-4CEE-8E91-60E12A6C81A7}") );  
  44.         }  
  45.     }  
  46.   
  47.     HRESULT hr = _AtlModule.DllUnregisterServer();  
  48.     return hr;  
  49. }  
// 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进程。

展开阅读全文

没有更多推荐了,返回首页