(C#)Windows Shell 外壳编程系列总结

http://www.cnblogs.com/lemony/category/88555.html

原文见上面链接中的1-9系列

1、基础,浏览一个文件夹

win32中是以外壳名字空间的形式来组织文件系统的,在外壳名字空间里的每一个对象(注)都实现了一个IShellFolder的接口,通过这个接口我们可以直接查询或间接得到其他相关的接口。

(注:这里的对象指的是外壳名字空间中的一个节点,对象有可能是一个文件夹,有可能是一个文件,也有可能是一个虚拟文件夹,例如:我的电脑,网上邻居,控制面板等)

首先我们必须了解,在外壳编程中,要使用 PIDL 路径代替普通路径(如果对 PIDL 不熟悉,请看Windows外壳名字空间的浏览)。
“桌面”是最顶级的文件夹,外壳名字空间中其他各项都可以用从“桌面”开始的 PIDL 加以表示

如何获取“桌面”的 PIDL 和其 IShellFolder 接口呢,可以通过 API SHGetDesktopFolder:

首先是桌面的Ishellfolder 通过这个我们找我们自己目标路径的PIDL[IshellFolder可以通过 IShellFolder 的 ParseDisplayName 和 BindToObject 函数来实现:]

void Ishellfolder .GetDisplayNameOf( ntPtr pidl, SHGNO uFlags, IntPtr lpName)为ParseDisplayName 的逆函数,两者实现从文件名到pidl和从pidl到文件名的一个抓换

拿到了我们自己目标路径的ishellFolder  就可以通过EnumObjects 获得IEnumIDList进而通过循环获取其子项(子文件和子文件夹)的pidl:

获取到了pidl通过SHGetFileInfo来获取对应的文件或文件夹信息


2 - 解释,从“桌面”开始展开

对1的类似递归的利用,展开所有的文件夹及文件信息

类似“桌面”、“我的文档”这种既是普通文件夹又是特殊对象的绝对路径如何获得,这里就要用到 SHGetSpecialFolderPath API 了


3 - 上下文菜单(iContextMenu)(一)右键菜单

对象的上下文菜单相关的接口是IContextMenu,通过对象的父文件夹的IShellFolder.GetUIObjectOf方法可得到该接口。得到该接口后,可以用IContextMenu.QueryContextMenu方法来生成上下文菜单的菜单项,用IContextMenu.InvokeCommand调用相应的命令。

首先假设我们已经获得某个 IShellFolder 对象的 PIDL 和其上级 IShellFolder 对象:

IntPtr PIDL;
IShellFolder IParent;

然后我们定义一个存放 PIDL 的数组:

IntPtr[] pidls  =   new  IntPtr[ 1 ];
pidls[
0 =  PIDL;

没错,我们的确要用到 PIDL 数组。可以理解,你在资源管理器中选择了多个文件/文件夹,再点击右键,弹出的上下文菜单将有所不同。你可以根据需要,把同一级的多个 PIDL 放到数组里面,实现这个效果。由于我们在例2的树中弹出菜单,所以只存放一个节点的 PIDL。

然后,通过 IParent 的 GetUIObjectOf 方法我们可以得到该节点的一个或多个指定子节点的 IContextMenu 接口:

得到 IContextMenu 后我们需要提供一个弹出式菜单的句柄,并把他传给 IContextMenu.QueryContextMenu,如果该方法执行成功的话,会在我们的菜单里加入相应的菜单项。

// 提供一个弹出式菜单的句柄
IntPtr contextMenu  =  API.CreatePopupMenu();
iContextMenu.QueryContextMenu(contextMenu, 
0 ,
API.CMD_FIRST, API.CMD_LAST, CMF.NORMAL 
|  CMF.EXPLORE);

有了菜单项,我们就可以弹出该菜单了,我们用 TPM_RETURNCMD 标志指定 TrackPopupMenu 必须返回用户所选菜单项的 ID,以便稍后通过IContextMenu.InvokeCommand 来执行菜单命令:

// 弹出菜单
uint  cmd  =  API.TrackPopupMenuEx(contextMenu,TPM.RETURNCMD,
MousePosition.X, MousePosition.Y, 
this .Handle, IntPtr.Zero);

// 获取命令序号,执行菜单命令
if  (cmd  >=  API.CMD_FIRST)
{
    CMINVOKECOMMANDINFOEX invoke 
= new CMINVOKECOMMANDINFOEX();
    invoke.cbSize 
= Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX));
    invoke.lpVerb 
= (IntPtr)(cmd - 1);
    invoke.lpDirectory 
= string.Empty;
    invoke.fMask 
= 0;
    invoke.ptInvoke 
= new POINT(MousePosition.X, MousePosition.Y);
    invoke.nShow 
= 1;
    iContextMenu.InvokeCommand(
ref invoke);

}


IContextMenu Methods Description
GetCommandString Retrieves the language-independent name of a menu command or the Help text for a menu command.
InvokeCommand Carries out a menu command. For an example, see the IExtractIcon interface.
QueryContextMenu Adds commands to a context menu.

4 - 上下文菜单(iContextMenu)(二)嵌入菜单和执行命令

上一节说到如何弹出 IShellFolder 的上下文菜单,也就是 IContextMenu。有时候我们需要在这个菜单上面,加入一些属于自己的菜单项。举个例子,你打开资源管理器,查看左边目录树的右键菜单,会发现顶层多了一个折叠/展开的菜单项。好,我们也动手来加入这个菜单项。

The InsertMenu function inserts a new menu item into a menu, moving other items down the menu.

The InsertMenu function has been superseded by the InsertMenuItem function. You can still use InsertMenu, however, if you do not need any of the extended features of InsertMenuItem.

BOOL InsertMenu(
  HMENU hMenu,      // handle to menu
  UINT uPosition,   // menu item that new menu item precedes
  UINT uFlags,      // menu item flags
  UINT uIDNewItem,  // menu item identifier or handle to drop-down 
                    // menu or submenu
  LPCTSTR lpNewItem // menu item content
);
 


参数2表示增加菜单项的位置,从0开始。

参数3表示flag,这里提供了菜单状态,以及位置的计算方法,它是一个枚举:

参数4表示命令值。我们可以根据这个命令值来执行对应的功能。

执行菜单命令

能不能不弹出菜单直接调用菜单项相应的命令?答案是肯定的。

大家还记得怎么显示一个文件或文件夹的属性对话框吗?

Yes,用ShellExecuteEx并指定SHELLEXECUTEINFO的lpVerb域为properties就可,但是这种方法只能查看一个文件的属性,怎么同时查看多个的?

要知道ShellExecuteEx查看文件属性最终也是靠IContextMenu帮忙的,所以答案还是在IContextMenu上,我们只要在调用GetUIObjectOf时把想查看的文件或文件件的PIDL做为参数传进去,然后直接调用InvokeCommand方法就OK啦。

当然,我们做的例子,还是弹出一个对象的属性,靠你自己修改了。

我们必须先得到 IContextMenu 接口:

// 得到 IContextMenu 接口
IntPtr iContextMenuPtr  =  IntPtr.Zero;
iContextMenuPtr 
=  IParent.GetUIObjectOf(IntPtr.Zero, ( uint )pidls.Length,
    pidls, 
ref  Guids.IID_IContextMenu,  out  iContextMenuPtr);
IContextMenu iContextMenu 
=  (IContextMenu)Marshal.GetObjectForIUnknown(iContextMenuPtr);

但我们不弹出这个菜单,仅仅是调用 InvokeCommand 来执行命令而已:

// 直接执行命令
CMINVOKECOMMANDINFOEX invoke  =   new  CMINVOKECOMMANDINFOEX();
invoke.cbSize 
=  Marshal.SizeOf( typeof (CMINVOKECOMMANDINFOEX));
invoke.lpVerb 
=  Marshal.StringToHGlobalAnsi( " properties " );
invoke.lpDirectory 
=   string .Empty;
invoke.fMask 
=   0 ;
invoke.nShow 
=   1 ;
iContextMenu.InvokeCommand(
ref  invoke);

关于verb的更多信息请参考MSDN。我这里做的是执行“属性”,如果你要执行其他命令,或者按照索引来执行,也是可以的。这里不做深入研究。



5 - 获取图标

有关 PIDL 

  PIDL 亦有“绝对路径”与“相对路径”的概念。表示“相对路径”的 PIDL (本文简称为“相对 PIDL ”)只有一个 ITEMIDLIST 结构的元素,用于标识相对于父文件夹的“路径”;表示“绝对路径”的 PIDL (简称为“绝对 PIDL ”)有若干个 ITEMIDLIST 结构的元素,第一个元素表示外壳名字空间根文件夹(“桌面”)下的某一子文件夹 A ,第二个元素则表示文件夹 A 下的某一子文件夹 B ,其余依此类推。这样绝对 PIDL 就通过保存一条从“桌面”下的直接子文件夹或文件的绝对 PIDL 与相对 PIDL 是相同的,而其他的文件夹或文件的相对 PIDL 就只是其绝对 PIDL 的最后一部分了。

  为什么要说这些呢?因为有些函数,必须使用绝对PIDL,例如图标,如果不使用绝对PIDL,某些图标是无法正常获得的(驱动器、控制面板等)

    但使用 EnumObjects 获得的,仅仅是相对PIDL,如果通过相对PIDL获取绝对PIDL呢?我参考了开源项目 C# FileBrowser 中的 PIDL 类

private ShellItem m_ParentItem;

public ShellItem ParentItem
{
    
get return m_ParentItem; }
    
set { m_ParentItem = value; }
}


/// <summary>
/// 绝对 PIDL
/// </summary>

public PIDL PIDLFull
{
    
get
    
{
        PIDL pidlFull 
= new PIDL(PIDL, true);
        ShellItem current 
= ParentItem;
        
while (current != null)
        
{
            pidlFull.Insert(current.PIDL);
            current 
= current.ParentItem;
        }

        
return pidlFull;
    }

}

获取图标

    言归正传,既然已经获得绝对 PIDL,那么获取图标就是很简单的事情了,我们使用的是 SHGetFileInfo 这个API:

这里提供了一个重载,你可以选择是通过 PIDL 还是 路径 获取图标(如果是路径,那么仅仅能获取 文件夹/文件 的图标)。


大家也许会觉得奇怪,GetSmallIconIndex 返回的是 int ,到底要怎么使用?

其实没错,GetSmallIconIndex 仅仅是返回该图标在系统图像列表(System ImageList)的索引(Index)而已。我们只要获取系统图像列表的指针,再把它关联到

你的 TreeView 或 ListView ,即可通过 Icon Index 来显示图标了。

6 - 执行


资源管理器

    经过把前几节中的例子修改,大致得到一个资源管理器的原型,但它还有很多问题:

1,不会释放资源
2,无法显示快捷方式、共享等图标标志
3,ContextMenu 某些地方没有处理,例如发送到...
4,拖拉没有实现
5,没有实时监控更改

    因此,要做一个完整的资源管理器,是非常麻烦的事情,你可以参考 C# FileBrowser ,它已经做得非常好了。

http://www.codeproject.com/cs/miscctrl/FileBrowser.asp

7 - ContextMenu 注册文件右键菜单

从本节起,我所要讲述的是对 Windows 系统的“Shell 扩展”。“Shell 扩展”从字面上分两个部分:Shell 与 Extension。Shell 指 Windows Explorer,而Extension 则指由你编写的当某一预先约定好的事件(如在以. doc 为后缀的文件图标上单击右键)发生时由 Explorer 调用执行的代码。因此一个“Shell 扩展”就是一个为 Explorer 添加功能的 COM 对象。 


“Shell 扩展”有很多种类型,每种类型都在各自不同的事件发生时被调用运行,但也有一些扩展的类型和调用情形是非常相似的。
 

类型

何时被调用 应该作些什么

Context menu 
扩展处理器

用户右键单击文件或文件夹对象时,
或在一个文件夹窗口中的背景处单击右键时(要求
shell版本为4.71+
添加菜单项到上下文菜单中
Property sheet
扩展处理器

要显示一个文件对象的属性框时 添加定制属性页到属性表中
Drag and drop 
扩展处理器

用户用右键拖放文件对象到文件夹窗口或桌面时 添加菜单项到上下文菜单中
Drop 扩展处理器

用户拖动Shell对象并将它放到一个文件对象上时 任何想要的操作
QueryInfo扩展处理器 (需要shell版本 4.71+)

用户将鼠标盘旋于文件或其他Shell对象的图标上时

返回一个浏览器用于显示在提示框中的字符串

 

现在你可能想知道“Shell 扩展”到底是什么样的,不过我还是乐意把我后面所实现的技术效果直接展示出来。以下三副图片分别代表了三种“Shell 扩展”:

 

(1)实现类似 WinRAR 的右键菜单

 

(2)根据文本大小,显示不同的 TXT 文件图标

 

(3)当鼠标移动到 TXT 文件图标上的时候,显示内容预览。

 

好,废话不多说了,赶紧进入今天要讲述的内容: ContextMenu 注册文件右键菜单。

对于 WinRAR 所实现的效果,其实叫做上下文菜单。例如我们把扩展关联到 .TXT 文件,当用户右键单击文本文件对象时扩展就会被调用,然后向系统菜单增加菜单项,并响应相应的命令。由此可见,基本上每种 Shell 扩展,都需要做一些几乎一样的事情。

 初始化接口 

当我们的shell扩展被加载时,Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得一个 IShellExtInit 接口指针。该接口仅有一个方法 Initialize(),其函数原型为


[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e8-0000-0000-c000-000000000046")]
public interface IShellExtInit
{
    [PreserveSig()]
    
int Initialize(IntPtr pidlFolder, IntPtr lpdobj, uint hKeyProgID);
}

 

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

因此我们可以在这个方法中,获取到被右击选择的一个或多个文件/文件夹名。


protected ShellLib.IDataObject m_dataObject = null;
uint m_hDrop = 0;

int IShellExtInit.Initialize(IntPtr pidlFolder, IntPtr lpdobj, uint hKeyProgID)
{
    
try
    {
        m_dataObject 
= null;
        
if (lpdobj != (IntPtr)0)
        {
            m_dataObject 
= (ShellLib.IDataObject)Marshal.GetObjectForIUnknown(lpdobj);
            FORMATETC fmt 
= new FORMATETC();
            fmt.cfFormat 
= CLIPFORMAT.CF_HDROP;
            fmt.ptd 
= 0;
            fmt.dwAspect 
= DVASPECT.DVASPECT_CONTENT;
            fmt.lindex 
= -1;
            fmt.tymed 
= TYMED.TYMED_HGLOBAL;
            STGMEDIUM medium 
= new STGMEDIUM();
            m_dataObject.GetData(
ref fmt, ref medium);
            m_hDrop 
= medium.hGlobal;
        }
    }
    
catch (Exception)
    {
    }
    
return S_OK;
}

 

IDataObject 是一个接口,包含了一些获取文件名的方法,后面可以用到。


与上下文菜单交互的接口 

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


[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e4-0000-0000-c000-000000000046")]
public interface IContextMenu
{
    [PreserveSig()]
    
int QueryContextMenu(HMenu hmenu, uint iMenu, uint idCmdFirst, uint idCmdLast, CMF uFlags);
    [PreserveSig()]
    
void InvokeCommand(IntPtr pici);
    [PreserveSig()]
    
void GetCommandString(uint idcmd, GCS uflags, uint reserved, IntPtr commandstring, int cchMax);
}

第一个是 QueryContextMenu(), 它让我们可以修改上下文菜单。其参数:

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

 

我们调用该方法,为上下文菜单增加几个菜单项:

在状态栏上显示提示帮助

下一个要被调用的IContextMenu 方法是 GetCommandString().。如果用户是在浏览器窗口中右击文本文件,或选中一个文本文件后单击文件菜单时,状态栏会显示提示帮助。我们的 GetCommandString() 函数将返回一个帮助字符串供浏览器显示。

 

执行用户的选择 

IContextMenu 接口的最后一个方法是 InvokeCommand()。当用户点击我们添加的菜单项时该方法将被调用。其参数:

CMINVOKECOMMANDINFO 结构带有大量的信息, 但我们只关心 lpVerb 和 hwnd 这两个成员。
lpVerb参数有两个作用 – 它或是可被激发的verb(动作)名, 或是被点击的菜单项的索引值。

hwnd 是用户激活我们的菜单扩展时所在的浏览器窗口的句柄。

 

我们可以根据被点击的菜单项索引,来执行相应的操作。

 

注册Shell扩展
现在我们已经实现了所有需要的COM接口. 可是我们怎样才能让浏览器使用我们的扩展呢?首先,我们要注册动态库。但仅仅这样是不够的,为了告诉浏览器使用我们的扩展, 我们需要在文本文件类型的注册表键下注册扩展。

(请原谅我未能抽出时间对注册扩展做详细的说明(如果以后有机会会补上),大家可以自行研究)

注册动态库

.NET 开发的动态库有些特别,需要在 .NET SDK 中注册

regasm MyContextMenu.dll /CodeBase
反注册则是:regasm /unregister MyContextMenu.dll /CodeBase

关于代码:代码里面还包括了图标扩展和提示扩展的代码,如果有兴趣,可自行阅读。

 

题外话:还有相当多的关于 Shell 扩展的内容无法一一说明,如果有机会,以后会尽量补上。或大家查阅网上的“Windows Shell扩展编程完全指南”(虽然是VC版的,但内容相当丰富)


7 - ContextMenu 注册文件右键菜单


 //获取系统 ImageList
            SHFILEINFO shfi = new SHFILEINFO();
            m_ipSmallSystemImageList = API.SHGetFileInfo("", 0, out shfi, Marshal.SizeOf(typeof(SHFILEINFO)),
                SHGFI.SYSICONINDEX | SHGFI.SMALLICON | SHGFI.USEFILEATTRIBUTES);
            m_ipLargeSystemImageList = API.SHGetFileInfo("", 0, out shfi, Marshal.SizeOf(typeof(SHFILEINFO)),
                SHGFI.SYSICONINDEX | SHGFI.LARGEICON | SHGFI.USEFILEATTRIBUTES);

            //把系统 ImageList 关联到 TreeView 和 ListView
            //通过发送消息的方式绑定imagelist
            API.SendMessage(Tree1.Handle, API.TVM_SETIMAGELIST, API.TVSIL_NORMAL, m_ipSmallSystemImageList);

            API.SendMessage(lvFile.Handle, API.LVM_SETIMAGELIST, API.LVSIL_NORMAL, m_ipLargeSystemImageList);

对于WIN7,暂时没有找到合适的注册右键菜单的shell编程注册解决方案

2018年3月31日16:11:03 

github 上有个demo 待测试 https://github.com/blak3r/openop    /CSShellExtContextMenuHandler

2018年3月31日16:28:32

C:\Windows\Microsoft.NET\Framework64\v4.0.30319>Regasm.exe C:\tmp\openop-master\
CSShellExtContextMenuHandler\bin\x64\Debug\CSShellExtContextMenuHandler.dll /cod
ebase
Microsoft .NET Framework 程序集注册实用工具版本 4.7.2558.0
(适用于 Microsoft .NET Framework 版本 4.7.2558.0)
版权所有 (C) Microsoft Corporation。保留所有权利。


成功注册了类型


C:\Windows\Microsoft.NET\Framework64\v4.0.30319>Regasm.exe C:\tmp\openop-master\
CSShellExtContextMenuHandler\bin\x64\Debug\CSShellExtContextMenuHandler.dll /unr
egister
Microsoft .NET Framework 程序集注册实用工具版本 4.7.2558.0
(适用于 Microsoft .NET Framework 版本 4.7.2558.0)
版权所有 (C) Microsoft Corporation。保留所有权利。


成功注销了类型


C:\Windows\Microsoft.NET\Framework64\v4.0.30319>           


虽然现实成果注册了,但是右键还是没有出来。。。。。。。。。                   


猜你喜欢

转载自blog.csdn.net/jerryzfc/article/details/79766517