首页  ·  知识 ·  编程语言
WindowsShell外壳编程系列7-ContextMenu注册文件右键菜单
lemony   http://www.cnblogs.com/lemony/archive/2008/08/29/1279588.html  .NET  编辑:德仔   图片来源:网络
display: none id=1268717948818S/从本节起,我所要讲述的是对 Win

从本节起,我所要讲述的是对 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 文件图标上的时候,显示内容预览。
 
 
 
对于 WinRAR 所实现的效果,其实叫做上下文菜单。例如我们把扩展关联到 .TXT 文件,当用户右键单击文本文件对象时扩展就会被调用,然后向系统菜单增加菜单项,并响应相应的命令。由此可见,基本上每种 Shell 扩展,都需要做一些几乎一样的事情。
 
初始化接口
当我们的shell扩展被加载时,Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得一个 IShellExtInit 接口指针。该接口仅有一个方法 Initialize(),其函数原型为:  
 
IShellExtInit原型
[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 是我们应该添加菜单项的起始位置。
uidFirstCmduidLastCmd 是我们可以使用的菜单命令ID值的范围。
uFlags 标识了Explorer 调用 QueryContextMenu() 的原因。

 

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

int IContextMenu.QueryContextMenu(HMenu hMenu, uint iMenu, uint idCmdFirst, uint idCmdLast, CMF uFlags)
{
    
int id = 0;
    
if ((uFlags & (CMF.CMF_VERBSONLY | CMF.CMF_DEFAULTONLY | CMF.CMF_NOVERBS)) == 0 ||
        (uFlags 
& CMF.CMF_EXPLORE) != 0)
    {
        
//创建子菜单
        HMenu submenu = ShellLib.Helpers.CreatePopupMenu();
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING, 
new IntPtr(idCmdFirst + id++), "复制路径(&C)");
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING, 
new IntPtr(idCmdFirst + id++), "复制文本内容(&T)");
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING, 
new IntPtr(idCmdFirst + id++), "柠檬的博客(&L)");

        
//将子菜单插入到上下文菜单中
        Helpers.InsertMenu(hMenu, 1, MFMENU.MF_BYPOSITION | MFMENU.MF_POPUP, submenu.handle, "MyContextMenu(&Y)");

        
//为菜单增加图标
        Bitmap bpCopy = Resource1.copy;
        Helpers.SetMenuItemBitmaps(submenu, 
0, MFMENU.MF_BYPOSITION, bpCopy.GetHbitmap(), bpCopy.GetHbitmap());
        Helpers.SetMenuItemBitmaps(submenu, 
1, MFMENU.MF_BYPOSITION, bpCopy.GetHbitmap(), bpCopy.GetHbitmap());
        Bitmap bpHome 
= Resource1.home;
        Helpers.SetMenuItemBitmaps(submenu, 
2, MFMENU.MF_BYPOSITION, bpHome.GetHbitmap(), bpHome.GetHbitmap());
    }
    
return id;
}

 

在状态栏上显示提示帮助

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

void IContextMenu.GetCommandString(uint idcmd, GCS uflags, uint reserved, IntPtr commandstring, int cchMax)
{
    
string tip = "";

    
switch (uflags)
    {
        
case GCS.VERB:
            
break;
        
case GCS.HELPTEXTW:
            
switch (idcmd)
            {
                
case 0:
                    tip 
= "把选中的文件/文件夹的全路径复制到剪切板";
                    
break;
                
case 1:
                    tip 
= "把选中的 TXT 文本内容复制到剪切板";
                    
break;
                
case 2:
                    tip 
= "访问柠檬的博客 http://lemony.cnblogs.com";
                    
break;
                
default:
                    
break;
            }
            
if (!string.IsNullOrEmpty(tip))
            {
                
byte[] data = new byte[cchMax * 2];
                Encoding.Unicode.GetBytes(tip, 
0, tip.Length, data, 0);
                Marshal.Copy(data, 
0, commandstring, data.Length);
            }
            
break;
    }
}

 

执行用户的选择

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

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

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

 

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

void IContextMenu.InvokeCommand(IntPtr pici)
{
    INVOKECOMMANDINFO ici 
= (INVOKECOMMANDINFO)Marshal.PtrToStructure(pici, typeof(ShellLib.INVOKECOMMANDINFO));
    StringBuilder sb 
= new StringBuilder(1024);
    StringBuilder sbAll 
= new StringBuilder();
    
uint nselected;

    
switch (ici.verb)
    {
        
case 0:
            
//复制文件名
            nselected = Helpers.DragQueryFile(m_hDrop, 0xffffffffnull0);
            
for (uint i = 0; i < nselected; i++)
            {
                ShellLib.Helpers.DragQueryFile(m_hDrop, i, sb, sb.Capacity 
+ 1);
                sbAll.Append(sb.ToString() 
+ "\n");
            }
            Clipboard.Clear();
            Clipboard.SetDataObject(sbAll.ToString(), 
true);
            
break;
        
case 1:
            
//复制文件内容
            nselected = Helpers.DragQueryFile(m_hDrop, 0xffffffffnull0);
            
for (uint i = 0; i < nselected; i++)
            {
                ShellLib.Helpers.DragQueryFile(m_hDrop, i, sb, sb.Capacity 
+ 1);
                StreamReader sr 
= new StreamReader(sb.ToString(), Encoding.GetEncoding("gb2312"));
                sbAll.Append(sr.ReadToEnd());
                sr.Close();
            }
            Clipboard.Clear();
            Clipboard.SetDataObject(sbAll.ToString(), 
true);
            
break;
        
case 2:
            
//调用浏览器,打开网页
            Process proc = new Process();
            proc.StartInfo.FileName 
= "IExplore.exe";
            proc.StartInfo.Arguments 
= "http://lemony.cnblogs.com";
            proc.Start();
            
break;
        
default:
            
break;
    }
}

 

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

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

 

System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
static void RegisterServer(String str1)
{
    
try
    {
        
//注册 DLL
        RegistryKey root;
        RegistryKey rk;
        root 
= Registry.LocalMachine;
        rk 
= root.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"true);
        rk.SetValue(GUID, KEYNAME);
        rk.Close();
        root.Close();

        
//注册文件
        RegTXT();
    }
    
catch{
    }
}

[System.Runtime.InteropServices.ComUnregisterFunctionAttribute()]
static void UnregisterServer(String str1)
{
    
try
    {
        
//注销动态库
        RegistryKey root;
        RegistryKey rk;
        root 
= Registry.LocalMachine;
        rk 
= root.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved"true);
        rk.DeleteValue(GUID);
        rk.Close();
        root.Close();

        
//注销文件
        UnRegTXT();
    }
    
catch
    {
    }
}

private static void RegTXT()
{
    RegistryKey root;
    RegistryKey rk;

    root 
= Registry.ClassesRoot;
    rk 
= root.OpenSubKey(".txt");
    
string txtclass = (string)rk.GetValue("");
    
if (string.IsNullOrEmpty(txtclass))
    {
        txtclass 
= "TXT";
        rk.SetValue(
"", txtclass);

    }
    rk.Close();

    rk 
= root.CreateSubKey(txtclass + "\\shellex\\ContextMenuHandlers\\" + KEYNAME);
    rk.SetValue(
"", GUID);
    rk.Close();

    rk 
= root.CreateSubKey(txtclass + "\\shellex\\IconHandler");
    rk.SetValue(
"", GUID);
    rk.Close();

    rk 
= root.CreateSubKey(txtclass + "\\shellex\\{00021500-0000-0000-C000-000000000046}");
    rk.SetValue(
"", GUID);
    rk.Close();
}

private static void UnRegTXT()
{
    RegistryKey root;
    RegistryKey rk;

    root 
= Registry.ClassesRoot;
    rk 
= root.OpenSubKey(".txt");
    rk.Close();
    
string txtclass = (string)rk.GetValue("");
    
if (!string.IsNullOrEmpty(txtclass))
    {
        root.DeleteSubKey(txtclass 
+ "\\shellex\\ContextMenuHandlers\\" + KEYNAME);
        root.DeleteSubKey(txtclass 
+ "\\shellex\\IconHandler");
        root.DeleteSubKey(txtclass 
+ "\\shellex\\{00021500-0000-0000-C000-000000000046}");
    }
}


注册动态库

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

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

 

代码:http://files.cnblogs.com/lemony/MyContextMenu.rar

 
本文作者:lemony 来源: http://www.cnblogs.com/lemony/archive/2008/08/29/1279588.html
CIO之家 www.ciozj.com 微信公众号:imciow
   
免责声明:本站转载此文章旨在分享信息,不代表对其内容的完全认同。文章来源已尽可能注明,若涉及版权问题,请及时与我们联系,我们将积极配合处理。同时,我们无法对文章内容的真实性、准确性及完整性进行完全保证,对于因文章内容而产生的任何后果,本账号不承担法律责任。转载仅出于传播目的,读者应自行对内容进行核实与判断。请谨慎参考文章信息,一切责任由读者自行承担。
延伸阅读