zoukankan      html  css  js  c++  java
  • (C#)Windows Shell 外壳编程系列7 ContextMenu 注册文件右键菜单

    (本系列文章由柠檬的(lc_mtt)原创,转载请注明出处,谢谢~)

    从本节起,我所要讲述的是对 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(),其函数原型为:  

    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的注册数据。

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

    IShellExtInit.Initialize
    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 接口定义了以下几个方法:

    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.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() 函数将返回一个帮助字符串供浏览器显示。

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

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

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

    IContextMenu.InvokeCommand
    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, 0xffffffff, null, 0);
    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, 0xffffffff, null, 0);
    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

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

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

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

  • 相关阅读:
    POJ 2240 Arbitrage spfa 判正环
    POJ 3259 Wormholes spfa 判负环
    POJ1680 Currency Exchange SPFA判正环
    HDU5649 DZY Loves Sorting 线段树
    HDU 5648 DZY Loves Math 暴力打表
    HDU5647 DZY Loves Connecting 树形DP
    CDOJ 1071 秋实大哥下棋 线段树
    HDU5046 Airport dancing links 重复覆盖+二分
    HDU 3335 Divisibility dancing links 重复覆盖
    FZU1686 神龙的难题 dancing links 重复覆盖
  • 原文地址:https://www.cnblogs.com/MaxWoods/p/1764036.html
Copyright © 2011-2022 走看看