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

来源:互联网 发布:quik是什么软件 编辑:程序博客网 时间:2024/04/30 14:12

作者:朱金灿

来源: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:

// IContextMenuSTDMETHOD(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_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() 如下:

#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 结构带有大量的信息, 但我们只关心 lpVerbhwnd 这两个成员.
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进程。

 

 

参考文献:

 

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

 

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


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

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

参与投票有机会获奖:

     最佳贡献奖:通过微博分享活动就有机会获得30元充值卡一张(每周抽选5名)
    幸运奖:凡参与投票用户就有机会获得精美小礼品一份。(每周抽选5名)
    积极参与奖:所有参与投票并符合条件的用户均可获得20个下载积分。






原创粉丝点击