首页 > 解决方案 > 使用 FolderItemVerbs 并调用或 DoIt 从 VBA 执行 windows shell 中的子菜单项

问题描述

下面的代码显示了一种获取文件夹中所有文件的所有动词(上下文菜单或右键单击菜单中可用的命令)的方法。示例:如果我想通过使用 VBA 中的右键菜单“编辑”来调用文件的“编辑”命令,则下面的代码可以正常工作。我可以简单地列出文件(这是一个文件夹项)可用的所有动词,然后如果它是命令编辑则调用它。到目前为止,一切都很好。

但是,如果我想调用一个子菜单(例如“使用/写字板打开”),那么该子菜单的动词就是空白。下面的例程将打印“子菜单”以表明我们可以知道有一个子菜单,但我不知道该子菜单中有什么可用的命令,也不能调用它们。

有人知道该怎么做吗?FolderItemVerbs 接口似乎只捕获上下文菜单的第一级,而不是任何子菜单。

Dim MySh As Object
Dim objFolder2 As Folder2

Set MySh = CreateObject("Shell.Application")
Set objFolder2 = MySh.Namespace(follow_direc)

If (Not objFolder2 Is Nothing) Then
    Dim objFolderItem As FolderItem
    Dim objItemVerbs As FolderItemVerbs
    Dim objVerb As FolderItemVerb

    For Each objFolderItem In objFolder2.Items
        If (Not objFolderItem Is Nothing) Then
            Debug.Print objFolderItem.Verbs.Count
            Set objItemVerbs = objFolderItem.Verbs

            If (Not objItemVerbs Is Nothing) Then
                For Each objVerb In objItemVerbs
                    If Len(objVerb.Name) > 1 Then
                        Debug.Print objVerb.Name
                    Else
                        Debug.Print "submenu"
                    End If
                Next
            End If
            Set objItemVerbs = Nothing
        Else
            'FolderItem object returned nothing.
        End If
    Next

    Set objFolderItem = objFolder2.Self
    Set objFolderItem = Nothing
Else
    'Folder object returned nothing.
End If
Set objFolder2 = Nothing
Set MySh = Nothing

标签: vbams-accesscontextmenums-access-2016windows-shell

解决方案


虽然Scriptable Shell Objects非常有用(并且为许多开发人员所熟知......),包括 .NET,但它们并不完全支持上下文项和子项(看起来微软已经对这个 COM 实用程序失去了兴趣很长时间了现在有时间)。

所以,这里有一个对这些有更好支持的 .NET 类(对不起,它是 C#,但您应该能够将其转换为 VB.NET)。

以下是如何在控制台应用程序中列出给定文件的菜单项层次结构:

class Program
{
    // [STAThread] things can vary with that set or not...
    static void Main(string[] args)
    {
        foreach (var item in ShellMenuItem.ExtractMenu(@"c:\mypath\myfile.txt"))
        {
            Dump(0, item);
        }
    }

    static void Dump(int indent, ShellMenuItem item)
    {
        var s = new string(' ', indent);
        if (item.IsSeparator)
        {
            Console.WriteLine("-");
            return;
        }
        Console.WriteLine(s + item.Text);
        Console.WriteLine(s + " id:" + item.Id);
        Console.WriteLine(s + " state:" + item.State);
        Console.WriteLine(s + " type:" + item.Type);
        Console.WriteLine(s + " verb:" + item.Verb);
        foreach (var child in item.Items)
        {
            Dump(indent + 1, child);
            Console.WriteLine();
        }

        if (item.Items.Count == 0)
        {
            Console.WriteLine();
        }
    }
}

以下是如何从 Windows 窗体应用程序调用文件的“属性”菜单项:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        ShellMenuItem.InvokeMenuItem(@"c:\mypath\myfile.txt", item => item.Verb == "properties");
    }
}

请注意,它如何工作(或不工作)很大程度上取决于许多上下文参数,例如进程位数(32 位或 64 位)、进程类型(控制台与 Windows)或当前线程的 COM 单元状态(STA 与 MTA 等) .)。它还取决于动态上下文菜单处理程序如何选择添加或不添加菜单项。

例如,如果您知道 Notepad++,则“使用 Notepad++ 编辑”条目仅在控制台模式下列出,因此只能从控制台应用程序调用。这是一个例外,因为大多数标准 Shell 菜单项(如“属性”)仅适用于 Windowed 应用程序。

public sealed class ShellMenuItem
{
    private List<ShellMenuItem> _items = new List<ShellMenuItem>();

    private ShellMenuItem()
    {
    }

    public int Id { get; private set; }
    public string Text { get; private set; }
    public string Verb { get; private set; }
    public MFS State { get; private set; }
    public MFT Type { get; private set; }
    public bool IsSeparator => Type.HasFlag(MFT.MFT_SEPARATOR);
    public IReadOnlyList<ShellMenuItem> Items => _items;

    public override string ToString() => IsSeparator ? "-" : Text;

    public static IReadOnlyList<ShellMenuItem> ExtractMenu(string path)
    {
        if (path == null)
            throw new ArgumentNullException(nameof(path));

        var list = new List<ShellMenuItem>();
        ExtractMenu(path, (parent, item) =>
        {
            if (parent == null)
            {
                list.Add(item);
            }
            else
            {
                parent._items.Add(item);
            }
        });
        return list;
    }

    public static void ExtractMenu(string path, Action<ShellMenuItem, ShellMenuItem> action)
    {
        if (path == null)
            throw new ArgumentNullException(nameof(path));

        if (action == null)
            throw new ArgumentNullException(nameof(action));

        ExtractMenu(path, (parent, item, cm) => action(parent, item));
    }

    private static void ExtractMenu(string path, Action<ShellMenuItem, ShellMenuItem, IContextMenu2> action)
    {
        int hr = SHCreateItemFromParsingName(path, null, typeof(IShellItem).GUID, out var item);
        if (hr < 0)
            throw new Win32Exception(hr);

        var pai = (IParentAndItem)item;

        hr = pai.GetParentAndItem(out var folderPidl, out var folder, out var itemPidl);
        if (hr < 0)
            throw new Win32Exception(hr);

        hr = folder.GetUIObjectOf(IntPtr.Zero, 1, new[] { itemPidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out var obj);
        if (hr < 0)
            throw new Win32Exception(hr);

        var menu = CreateMenu();
        try
        {
            var cm = (IContextMenu2)obj;
            hr = cm.QueryContextMenu(menu, 0, 0, 0x7FFF, CMF.CMF_NORMAL);
            if (hr < 0)
                throw new Win32Exception(hr);

            ExtractMenu(path, cm, menu, null, action);
        }
        finally
        {
            DestroyMenu(menu);
            Marshal.FreeCoTaskMem(folderPidl);
            Marshal.FreeCoTaskMem(itemPidl);
        }
    }

    public static void InvokeMenuItem(string path, Func<ShellMenuItem, bool> predicate) => InvokeMenuItem(path, IntPtr.Zero, predicate);
    public static void InvokeMenuItem(string path, IntPtr hwnd, Func<ShellMenuItem, bool> predicate)
    {
        if (path == null)
            throw new ArgumentNullException(nameof(path));

        if (predicate == null)
            throw new ArgumentNullException(nameof(predicate));

        ExtractMenu(path, (parent, item, cm) =>
        {
            if (predicate(item))
            {
                var info = new CMINVOKECOMMANDINFOEX();
                info.cbSize = Marshal.SizeOf(info);
                info.hwnd = hwnd;
                info.lpVerb = new IntPtr(item.Id);
                int hr = cm.InvokeCommand(ref info);
                if (hr < 0)
                    throw new Win32Exception(hr);
            }
        });
    }

    private static void ExtractMenu(string path, IContextMenu2 cm, IntPtr menuHandle, ShellMenuItem parent,
        Action<ShellMenuItem, ShellMenuItem, IContextMenu2> action)
    {
        int count = GetMenuItemCount(menuHandle);
        for (int i = 0; i < count; i++)
        {
            var mii = new MENUITEMINFO();
            mii.cbSize = Marshal.SizeOf(typeof(MENUITEMINFO));
            mii.fMask = MIIM.MIIM_FTYPE | MIIM.MIIM_ID | MIIM.MIIM_STATE | MIIM.MIIM_STRING | MIIM.MIIM_SUBMENU | MIIM.MIIM_DATA;
            if (!GetMenuItemInfo(menuHandle, i, true, ref mii))
                throw new Win32Exception(Marshal.GetLastWin32Error());

            if (mii.fType == MFT.MFT_STRING)
            {
                mii.dwTypeData = new string('\0', (mii.cch + 1) * 2);
                mii.cch++;
                if (!GetMenuItemInfo(menuHandle, i, true, ref mii))
                    throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            var item = new ShellMenuItem();
            item.Text = mii.dwTypeData;
            item.Id = mii.wID;
            item.Type = mii.fType;
            item.State = mii.fState;

            if (!item.IsSeparator)
            {
                var sb = new StringBuilder(256);
                cm.GetCommandString(new IntPtr(item.Id), GCS_VERBW, IntPtr.Zero, sb, sb.Capacity);
                if (!string.IsNullOrWhiteSpace(sb.ToString()))
                {
                    item.Verb = sb.ToString();
                }

                if (mii.hSubMenu != IntPtr.Zero)
                {
                    ExtractMenu(path, cm, mii.hSubMenu, item, action);
                }
            }
            action(parent, item, cm);
        }
    }

    private const int GCS_VERBW = 4;

    [DllImport("shell32", CharSet = CharSet.Unicode)]
    private static extern int SHCreateItemFromParsingName(string path, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv);

    [DllImport("user32")]
    private static extern IntPtr CreateMenu();

    [DllImport("user32")]
    private static extern bool DestroyMenu(IntPtr hMenu);

    [DllImport("user32")]
    private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos);

    [DllImport("user32")]
    private static extern int GetMenuItemCount(IntPtr hMenu);

    [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool GetMenuItemInfo(IntPtr hMenu, int uItem, bool fByPosition, ref MENUITEMINFO pmii);

    [Flags]
    private enum MIIM
    {
        MIIM_STATE = 0x00000001,
        MIIM_ID = 0x00000002,
        MIIM_SUBMENU = 0x00000004,
        MIIM_CHECKMARKS = 0x00000008,
        MIIM_TYPE = 0x00000010,
        MIIM_DATA = 0x00000020,
        MIIM_STRING = 0x00000040,
        MIIM_BITMAP = 0x00000080,
        MIIM_FTYPE = 0x00000100,
    }

    [Flags]
    private enum CMF
    {
        CMF_NORMAL = 0x00000000,
        CMF_DEFAULTONLY = 0x00000001,
        CMF_VERBSONLY = 0x00000002,
        CMF_EXPLORE = 0x00000004,
        CMF_NOVERBS = 0x00000008,
        CMF_CANRENAME = 0x00000010,
        CMF_NODEFAULT = 0x00000020,
        CMF_INCLUDESTATIC = 0x00000040,
        CMF_ITEMMENU = 0x00000080,
        CMF_EXTENDEDVERBS = 0x00000100,
        CMF_DISABLEDVERBS = 0x00000200,
        CMF_ASYNCVERBSTATE = 0x00000400,
        CMF_OPTIMIZEFORINVOKE = 0x00000800,
        CMF_SYNCCASCADEMENU = 0x00001000,
        CMF_DONOTPICKDEFAULT = 0x00002000,
        CMF_UNDOCUMENTED1 = 0x00004000,
        CMF_DVFILE = 0x10000,
        CMF_UNDOCUMENTED2 = 0x20000,
        CMF_RESERVED = unchecked((int)0xffff0000)
    }

    [Flags]
    public enum CMIC_MASK
    {
        CMIC_MASK_ASYNCOK = 0x00100000,
        CMIC_MASK_HOTKEY = 0x00000020,
        CMIC_MASK_FLAG_NO_UI = 0x00000400,
        CMIC_MASK_UNICODE = 0x00004000,
        CMIC_MASK_NO_CONSOLE = 0x00008000,
        CMIC_MASK_NOASYNC = 0x00000100,
        CMIC_MASK_SHIFT_DOWN = 0x10000000,
        CMIC_MASK_CONTROL_DOWN = 0x40000000,
        CMIC_MASK_FLAG_LOG_USAGE = 0x04000000,
        CMIC_MASK_NOZONECHECKS = 0x00800000,
        CMIC_MASK_PTINVOKE = 0x20000000,
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct MENUITEMINFO
    {
        public int cbSize;
        public MIIM fMask;
        public MFT fType;
        public MFS fState;
        public int wID;
        public IntPtr hSubMenu;
        public IntPtr hbmpChecked;
        public IntPtr hbmpUnchecked;
        public IntPtr dwItemData;
        public string dwTypeData;
        public int cch;
        public IntPtr hbmpItem;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct CMINVOKECOMMANDINFOEX
    {
        public int cbSize;
        public CMIC_MASK fMask;
        public IntPtr hwnd;
        public IntPtr lpVerb;
        [MarshalAs(UnmanagedType.LPStr)]
        public string lpParameters;
        [MarshalAs(UnmanagedType.LPStr)]
        public string lpDirectory;
        public int nShow;
        public int dwHotKey;
        public IntPtr hIcon;
        [MarshalAs(UnmanagedType.LPStr)]
        public string lpTitle;
        public IntPtr lpVerbW;
        public string lpParametersW;
        public string lpDirectoryW;
        public string lpTitleW;
        public long ptInvoke;
    }

    [Guid("000214e4-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IContextMenu
    {
        // we don't need anything from this, all is in IContextMenu2
    }

    [Guid("000214f4-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IContextMenu2
    {
        // IContextMenu
        [PreserveSig]
        int QueryContextMenu(IntPtr hmenu, int indexMenu, int idCmdFirst, int idCmdLast, CMF uFlags);

        [PreserveSig]
        int InvokeCommand(ref CMINVOKECOMMANDINFOEX pici);

        [PreserveSig]
        int GetCommandString(IntPtr idCmd, int uType, IntPtr pReserved, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMax);

        // IContextMenu2
        [PreserveSig]
        int HandleMenuMsg(int uMsg, IntPtr wParam, IntPtr lParam);
    }

    [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellItem
    {
        // we don't need anything from this
    }

    [Guid("000214e6-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellFolder
    {
        void _VtblGap1_7(); // skip 7 methods we don't need

        [PreserveSig]
        int GetUIObjectOf(IntPtr hwndOwner, int cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, IntPtr rgfReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
    }

    [Guid("b3a4b685-b685-4805-99d9-5dead2873236"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IParentAndItem
    {
        void _VtblGap1_1(); // skip 1 method we don't need

        [PreserveSig]
        int GetParentAndItem(out IntPtr ppidlParent, out IShellFolder ppsf, out IntPtr ppidlChild);
    }
}

[Flags]
public enum MFS
{
    MFS_GRAYED = 3,
    MFS_CHECKED = 8,
    MFS_HILITE = 128,
    MFS_ENABLED = 0,
    MFS_UNCHECKED = 0,
    MFS_UNHILITE = 0,
    MFS_DEFAULT = 4096,
}

[Flags]
public enum MFT
{
    MFT_STRING = 0,
    MFT_BITMAP = 4,
    MFT_MENUBARBREAK = 32,
    MFT_MENUBREAK = 64,
    MFT_RADIOCHECK = 512,
    MFT_SEPARATOR = 2048,
    MFT_RIGHTORDER = 8192,
    MFT_RIGHTJUSTIFY = 16384,
}

推荐阅读