VC下轻松实现扩展shell(一)

本节源码

 

VC 2005的用户需要注意的事项:The Express edition of VC 2005只能支持ATL和MFC其中之一,而文章中的代码需要同时用到ATL和MFC,所以在Express edition of VC 2005下这些代码是不可用的.

VC 6的用户需要注意的事项:如果你用的是VC6,你必须更新过Platform SDK,你可以使用 web install version或者下载CAB files或者用 ISO image,并在本地进行安装,还注意别忘了在环境变量中设置SDK的包含路径.在Platform SDK 程序组内,可以找到Visual Studio Registration 文件夹.即使你用的是VC7或者VC8也要保持最新版的SDK.

VC 7的用户需要注意的事项:如果你没更新Platform SDK,那么一定要修改默认路径.确认第一行是$(VCInstallDir)PlatformSDK/include,下面一行是($VCInstallDir)include,如图所示:

 

介绍

扩展shell是在windows shell(Exploer)的基础上增加了一些功能而形成的一个COM对象.它里面包含多种扩展,后面的文档会简明的告诉你他们是什么.(虽然从我最初写到现在的5年间情况发生了很大的转变)我还是很想给你介绍一本Dino Esposito's 的伟大著作<<Visual C++ Windows Shell Programming>>,如果你想深入的了解shell,而手头又刚好没有这样一本书.或者你只关心扩展shell,那么我写的这篇文章会对你有所帮助的.但是你最好有com和atl的基础,如果想再学一下COM基础,那么可以看我的另一片文章Intro to COM

 

第一部分是扩展shell的一般介绍,一个简单的上下文扩展菜单会使你很有兴趣读完剩下的部分.

术语 "shell extension."包含两部分,Shell对Explorer的引用,和你写的代码(通过shell运行预定义的事件)的扩展引用.(例如,在DOC文件上右击).所以扩展shell是有Explorer特征的COM对象.

 

shell扩展是一个进程内服务器,它实现了与Explorer通信的接口.ATL是实现扩展和快速启动的最简单的方式,因为没有它你很难实现QueryInterface() and AddRef()代码,而且在基于Windows NT的操作系统上用ATL很容易实现对扩展Shell的调试.

 

有很多种Shelll扩展类型,每一种调用都对应不同的事件,这有很通用的几种类型.以及他们调用的位置.

Type

When it's invoked

What it does

Context menu handler

User right-clicks on a file or folder. In shell versions 4.71+, also invoked on a right-click in the background of a directory window.

Adds items to the context menu.

Property sheet handler

Properties dialog displayed for a file.

Adds pages to the property sheet.

Drag and drop handler

User right-drags items and drops them on a directory window or the desktop.

Adds items to the context menu.

Drop handler

User drags items and drops them on a file.

Any desired action.

QueryInfo handler (shell version 4.71+)

User hovers the mouse over a file or other shell object like My Computer.

Returns a string that Explorer displays in a tooltip.

 

第一部分

现在你很想知道扩展Shell在Explorer中的样子,一个例子是WinZip,它包括很多种扩展,其中之一就是上下文菜单句柄.下面是带有WinZip命令的压缩文件的上下文菜单

 [WinZip menu items - 4K]

 

WinZip包含增加菜单项的代码,提供飞行导航帮助(文本出现在Explorer的状态栏),当用户选择WinZip的命令时执行相应的动作.

 

WinZip也包含一个拖拽功能,它的类型与上下文相关菜单扩展Shell很类似,但是当用户用鼠标右键拖拽一个文件的时候它才被调用.

 [WinZip menu items - 2K]

还有许多其他类型(MS在每个版本的Windows中一直在增加),现在我们只注意上下文相关菜单扩展Shell,因为他们写起来很容易看起来很直接.

 

在我们开始写代码之前,还有一些使工作更简单的技巧,当你想要通过Explorer载入一个扩展Shell的时候,它会在内存中等待一会儿,这样就无法重新编译DLL.让Explore卸载扩展Shell,可以创建这个注册键:

HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/Explorer/AlwaysUnloadDLL

在9x系统里,你可以将默认值设为"1",在NT系统里,打开这个键:

HKEY_CURRENT_USER/Software/Microsoft/Windows/CurrentVersion/Explorer

创建一个名称为DesktopProcess的DWORD,并把值设为1.这会使桌面和任务栏运行在同一个进程内.使每一个运行在Explorer窗体内的进程并发.也就是说你可以用单个Explorer窗体调试,当你关闭的时候DLL自动卸载,避免任何文件正使用的问题.你还需要断开与服务器的链接,恢复所有的改变.

 

使用向导

    现在开始实现一个简单的例子,只是弹出一个简单的消息对话框.我们将它关联到Txt文件上,当我们在Txt文件上单击右键时,就会弹出这个对话框.

好了,现在马上开始!怎么办呢?我还没有告诉你神秘的扩展Shell的原理,没关系,我会一边写一边讲解的.我发现通过例子讲解原理更容易,马上开始codeing,

    先通过向导生成一个新的ATL COM程序,给它起名"SimpleExt",在向导中使用所有的默认设置.然后单击完成,现在就有了一个空的ATL项目,我们需要将扩展Shell的COM对象加进来.在ClassView中,右击SimpleExt类项,选择新的ATL对象,(in VC7,右击项选择Add|Add Class.)

    在ATL对象向导中,第一个面板中使用默认选项,点击下一步,在第二个面板中,在Short Name编辑框中输入"SimpleShlExt"

默认情况下,向导创建一个可以被C和脚本语言通过自动化访问的COM对象.我们的扩展Shell只是被Explorer使用,所以就需要改变一些设置来去掉自动化,返回属性页,把接口类型改为Custom,并且将Aggregation设为No:

点OK按钮,向导创建了一个CSimpleShlExt 类,并且实现了COM对象的一些基本代码,把类加到对象中.

 

初始化接口

当我们的扩展Shell被装载的时候,浏览器调用我们的QueryInterface() 函数来得到一个IShellExtInit 接口的指针.接口只有一个方法,Initialize(),它的原型是:

HRESULT IShellExtInit::Initialize (
  LPCITEMIDLIST pidlFolder,
  LPDATAOBJECT pDataObj,
  HKEY hProgID )

Explorer用这个方法传给我们多种信息,pidlFolder是包含被自动检测的文件的文件夹的PIDL.(PIDL[指向一个ID列表]是一个在Shell里识别对象的具有唯一标识的数据结构)pDataObj是一个IDataObject的接口指针,通过这个指针我们可以得到被自动检测到的文件的文件名.hProgID是一个HKEY,通过它我们可以访问包含我们的DLL的注册数据的注册键.在这个简单的Shell扩展例子中,我们只需要使用pDataObj参数.

    打开SimpleShlExt.h 文件,加入下面的内容,向导产生的一些COM相关的代码并不需要,因为我们不实现自己的接口代码,所以下面同时标出了可以删除的代码.

#include <shlobj.h>
#include
<comdef.h>

class ATL_NO_VTABLE CSimpleShlExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
  public ISimpleShlExt,
  public IShellExtInit
{
  BEGIN_COM_MAP(CSimpleShlExt)
    COM_INTERFACE_ENTRY(ISimpleShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
  END_COM_MAP()

ATL是通过COM_MAP来实现 QueryInterface(). 它告诉ATL其他程序可以访问我们的哪一个接口.

用Initialize()来代替类声明.还需要分配一个缓存来存储文件名.

protected:
  TCHAR m_szFile[MAX_PATH];

public:
  // IShellExtInit
  STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

然后在SimpleShlExt.cpp文件里,增加函数的定义.

STDMETHODIMP CSimpleShlExt::Initialize (
  LPCITEMIDLIST pidlFolder,
  LPDATAOBJECT pDataObj,
  HKEY hProgID )

怎样通过右击文件能得到文件的名称,如果被选中的文件多于一个,你可以通过pDataObj接口指针访问.

当你用WS_EX_ACCEPTFILES类型在窗体上拖拽文件的时候,文件名的存储格式是一样的.也就是说,我们用同一个API取得文件名(DragQueryFile()).在函数的开始我们取得包含在IDataObject接口里的数据的句柄.

HRESULT CSimpleShlExt::Initialize(...)
{
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 there
  // is no such data, return an error back to Explorer.
  if ( FAILED( pDataObj->GetData ( &fmt, &stg ) ))
    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;

注意对所有的程序进行错误检查是极其重要的.尤其是指针.因为我们的扩展Shell运行在浏览器的进程空间内,如果我们的代码崩溃了,浏览器也会被关闭.在9x下,这种崩溃可能需要重启机器.

 

现在我们有HDROP句柄,我们可以取得需要的文件名.

  // Sanity check – make sure there is at least one filename.
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
HRESULT hr = S_OK;
 
  if ( 0 == uNumFiles )
    {
    GlobalUnlock ( stg.hGlobal );
    ReleaseStgMedium ( &stg );
    return E_INVALIDARG;
    }

  // 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_INVALIDARG,当右击文件的时候浏览器就不会调用我们的扩展Shell了.如果返回I_OK,Explorer将再一次调用

QueryInterface()来取得另一个接口IContextMenu.

 

与上下文菜单相关的接口

一旦浏览器初始化了我们的扩展Shell,它就会调用IContextMenu方法让我们来增加菜单项,增加飞行导航帮助,最后实现用户的选择.

打开SimpleShlExt.h文件,增加下面的内容

class ATL_NO_VTABLE CSimpleShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
    public IShellExtInit,
    public IContextMenu
{
  BEGIN_COM_MAP(CSimpleShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IContextMenu)
  END_COM_MAP()

public:
  // IContextMenu
  STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT);
  STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
  STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);

修改上下文菜单

IContextMenu有三个方法,第一个是QueryContextMenu(),可以让我们来修改菜单,QueryContextMenu()的原型是:

 

HRESULT IContextMenu::QueryContextMenu (
  HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
  UINT uidLastCmd, UINT uFlags );

hmenu是一个上下文菜单的句柄,uMenuIndex 是增加菜单项的起始位置,uidFirstCmd and uidLastCmd是菜单项ID的范围,uFlags指出浏览器调用QueryContextMenu()的原因.

 

在返回值上有歧义.Dino Esposito's 的书里说它是被QueryContextMenu()增加的菜单项的数目, VC 6的MSDN说它是我们增加的最后一个菜单项的命令ID值加一.

我在代码里一直是按照Dino's的解释,而且运行良好.事实上他的解释类似于MSDN的在线帮助,

 

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中你可以找到全部的flags,但是对扩展Shell的上下文菜单来说,只有一个值重要:CMF_DEFAULTONLY,这个标识告诉扩展Shell只增加默认的菜单项,这就是为什么如果设成CMF_DEFAULTONLY 会立即返回0.

 

在状态栏上显示飞行导航帮助

下一个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() 文档会说的更清楚一点.

总之,GetCommandString()的作用是,如果Explorer想要一个飞行导航帮助字符串,我们可以通过GetCommandString()提供,如果浏览器需要一个动作,我们将不理睬这个请求,如果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转换宏,那么可以看这里Nish and my article on string wrapper classes,在Unicode字符串被传递到COM方法和OLE函数中用他们会更容易.

 

一个需要注意的很重要的事情是lstrcpyn() API保证目标字符串是非空的,它和CRT函数strncpy()不同,它不会在源字符串的末尾增加一个空结束符,我想你一直在用lstrcpyn().

 

实现用户的选择

IContextMenu 里最后一个方法是InvokeCommand(). 如果用户单击我们增加的菜单项,那么这个方法会被调用,InvokeCommand()方法的原型是:

HRESULT IContextMenu::InvokeCommand (
  LPCMINVOKECOMMANDINFO pCmdInfo );

CMINVOKECOMMANDINFO结构包含很多信息,但是我们只关心 lpVerb and hwnd,lpVerb 有双重用途,它即是我们调用的verb的名称,还可以作为一个索引告诉我们哪一项被点击.hwnd是浏览器窗体的句柄,我们可以将这个创体作为UI和显示的所有的窗体的复窗体.

 

因为我们只有一个菜单项,我们选择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;
    }
}

其他代码细节

在向导产生的代码中删除OLE自动化特征,首先我们从SimpleShlExt.rgs里删除一些注册的进入点.

HKCR
{
  SimpleExt.SimpleShlExt. 1 = s ' SimpleShlExt Class'
  {
    CLSID = s ' {5E2121EE-0300-11D4-8D3B-444553540000}'
  }
  SimpleExt.SimpleShlExt = s ' SimpleShlExt Class'
  {
    CLSID = s ' {5E2121EE-0300-11D4-8D3B-444553540000}'
    CurVer = s ' SimpleExt.SimpleShlExt.1'
  }
  NoRemove CLSID
  {
    ForceRemove {5E2121EE-0300-11D4-8D3B-444553540000} = s 'SimpleShlExt Class'
    {
      ProgID = s ' SimpleExt.SimpleShlExt.1'
      VersionIndependentProgID = s ' SimpleExt.SimpleShlExt'
      InprocServer32 = s '%MODULE%'
      {
        val ThreadingModel = s 'Apartment'
      }
      ' TypeLib' = s ' {73738B1C-A43E-47F9-98F0-A07032F2C558}'
    }
  }
}

我们也能从DLL的资源里删除类型库,单击View|Resource Includes 在Compile-time directives 框里,你能看到有一行包含类型库.

当VC警告修改includes的时候,移除这一行.

在VC7里,是在Resource View 标签里,右键单击SimpleExt.rc 文件夹,在菜单里打开Resource Includes.

 

现在我们删除了类型库,我们需要修改两行代码,告诉ATL它不需要对类型库做任何事情.打开SimpleExt.cpp找到DllRegisterServer() 函数,将RegisterServer() 参数改为FALSE.

STDAPI DllRegisterServer()
{
//...
  return _Module.RegisterServer( TRUE FALSE);
}

DllUnregisterServer()需要一个同样的改变.

STDAPI DllUnregisterServer()
{
//...
  return _Module.UnregisterServer( TRUE FALSE);
}

注册Shell扩展

现在我们已经实现了所有COM接口,但是,怎么才能使Explorer能使用我们的扩展Shell呢,为了告诉浏览器使用我们的扩展Shell代码,需要注册.

 

在这个键下,一个叫ShellEx的键有一列Shell扩展与文本文件的调用相关.在ShellEx下,ContextMenuHandlers 键控制一列上下文菜单扩展.每一个扩展在ContextMenuHandlers下创建一个子键.并且设置这个键的GUIDde的默认值.我们给自己的简单的扩展Shell程序设置这个键:

 

设置我们自己的默认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,,意思是如果键存在,新的键写之前就会被删除,这一行的剩余部分指定一个字符串(这就是"s"的意思),用来存储SimpleShlExt键的默认值.

 

在这里我插入一些意见,需要在下面注册我们的扩展Shell的键是HKEY_CLASSES_ROOT/txtfile,然而,名字"txtfile" 不是一个预定义的名称,如果你看HKEY_CLASSES_ROOT/.txt,会找到默认值的存储位置.

  • 因为"txtfile"可能不是正确的键名,我们就无法保证使用一个不变的脚本.
  • 一些其他的文本编辑器可能被安装,并且与.TXT文件关联,如果它改变了HKEY_CLASSES_ROOT/.txt键的默认值,所有现存的扩展Shell将停止工作.

对我来说这是一个设计瑕疵,我想微软也在这么想,因为近期被创建的扩展Shell,像QueryInfo 扩展Shell,有一个系统策略

用来阻止扩展Shell被加载,它有一个验证.它的列表存贮在:

 

在这个键里,我们创建了一个字符串值名称是我们的GUID.这个字符串的内容可以是任何东西,在我们代码的DllRegisterServer() and DllUnregisterServer()中做这些事情.我不给出代码了.因为它不仅仅是简单的注册访问.

 

调试扩展Shell

最后,如果你写更复杂的扩展Shell程序时,就一定会用到调试,打开你的项目设置,在Debug标签上,在Executable for debug session"文本框内,输入Explorer的全路径,如果你用的是NT,你已经设置了DesktopProcess,在你按F5执行的时候,一个新的Explorer窗体会被打开,

 

我们的程序看起来会是什么样子的呢?

1.增加项目以后的上下文菜单

 [SimpleShlExt menu item - 3K]

2.Explorer的状态栏显示

 [SimpleShlExt help text - 3K]

3.消息框,显示选中的文件的文件名.

 [SimpleShlExt msg box - 9K]

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值