// Credit due to Gong-Shell from which this was largely taken. #if !NETCOREAPP3_1 using System; using System.Collections.Generic; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; using Vanara.PInvoke; using static Vanara.PInvoke.Shell32; using static Vanara.PInvoke.User32; namespace Vanara.Windows.Shell { /// Provides support for displaying the context menu of a shell item. /// /// Use this class to display a context menu for a shell item, either as a popup menu, or as a main menu. /// /// To display a popup menu, simply call with the parent control and the position at which the menu should /// be shown. /// /// /// To display a shell context menu in a Form's main menu, call the method to populate the menu. In addition, you /// must intercept a number of special messages that will be sent to the menu's parent form. To do this, you must override like so: /// /// ///protected override void WndProc(ref Message m) { ///if ((m_ContextMenu == null) || (!m_ContextMenu.HandleMenuMessage(ref m))) { ///base.WndProc(ref m); ///} ///} /// /// Where m_ContextMenu is the being shown. /// Standard menu commands can also be invoked from this class, for example and . /// public class ShellContextMenu { private const int m_CmdFirst = 0x8000; private readonly IContextMenu2 m_ComInterface2; private readonly IContextMenu3 m_ComInterface3; private readonly MessageWindow m_MessageWindow; /// Initialises a new instance of the class. /// The item to which the context menu should refer. public ShellContextMenu(ShellItem item) : this(new ShellItem[] { item }) { } /// Initialises a new instance of the class. /// The items to which the context menu should refer. public ShellContextMenu(ShellItem[] items) { var pidls = new IntPtr[items.Length]; ShellFolder parent = null; for (var n = 0; n < items.Length; ++n) { pidls[n] = ILFindLastID((IntPtr)items[n].PIDL); if (parent is null) { if (items[n] == ShellFolder.Desktop) { parent = ShellFolder.Desktop; } else { parent = items[n].Parent; } } else { if (items[n].Parent != parent) { throw new Exception("All shell items must have the same parent"); } } } ComInterface = parent.IShellFolder.GetUIObjectOf(HWND.NULL, pidls); m_ComInterface2 = ComInterface as IContextMenu2; m_ComInterface3 = ComInterface as IContextMenu3; m_MessageWindow = new MessageWindow(this); } /// Gets the underlying COM interface. public IContextMenu ComInterface { get; set; } /// Handles context menu messages when the is displayed on a Form's main menu bar. /// /// /// To display a shell context menu in a Form's main menu, call the method to populate the menu with the /// shell item's menu items. In addition, you must intercept a number of special messages that will be sent to the menu's parent /// form. To do this, you must override like so: /// /// ///protected override void WndProc(ref Message m) { ///if ((m_ContextMenu == null) || (!m_ContextMenu.HandleMenuMessage(ref m))) { ///base.WndProc(ref m); ///} ///} /// /// Where m_ContextMenu is the being shown. /// /// The message to handle. /// /// if the message was a Shell Context Menu message, if not. If the method returns /// false, then the message should be passed down to the base class's method. /// public bool HandleMenuMessage(ref Message m) { try { if ((m.Msg == (int)WindowMessage.WM_COMMAND) && ((int)m.WParam >= m_CmdFirst)) { InvokeCommand((int)m.WParam - m_CmdFirst); return true; } else { if (m_ComInterface3 != null) { m_ComInterface3.HandleMenuMsg2((uint)m.Msg, m.WParam, m.LParam, out IntPtr result); m.Result = result; return true; } else if (m_ComInterface2 != null) { m_ComInterface2.HandleMenuMsg((uint)m.Msg, m.WParam, m.LParam); m.Result = IntPtr.Zero; return true; } } } catch { } return false; } /// Invokes the Copy command on the shell item(s). public void InvokeCopy() => InvokeVerb("copy"); /// Invokes the Copy command on the shell item(s). public void InvokeCut() => InvokeVerb("cut"); /// Invokes the Delete command on the shell item(s). public void InvokeDelete() { try { InvokeVerb("delete"); } catch (COMException e) { // Ignore the exception raised when the user cancels a delete operation. if (e.ErrorCode != (HRESULT)(Win32Error)Win32Error.ERROR_CANCELLED && e.ErrorCode != HRESULT.COPYENGINE_E_USER_CANCELLED) { throw; } } } /// Invokes the Paste command on the shell item(s). public void InvokePaste() => InvokeVerb("paste"); /// Invokes the Rename command on the shell item. public void InvokeRename() => InvokeVerb("rename"); /// Invokes the specified verb on the shell item(s). public void InvokeVerb(string verb) { var invoke = new CMINVOKECOMMANDINFOEX(); invoke.cbSize = (uint)Marshal.SizeOf(invoke); invoke.lpVerb = new SafeResourceId(verb); ComInterface.InvokeCommand(invoke); } /// Populates a with the context menu items for a shell item. /// /// If this method is being used to populate a Form's main menu then you need to call in the Form's /// message handler. /// /// The menu to populate. public void Populate(Menu menu) { RemoveShellMenuItems(menu); ComInterface.QueryContextMenu(menu.Handle, 0, m_CmdFirst, int.MaxValue, CMF.CMF_EXPLORE); } /// Shows a context menu for a shell item. /// The parent control. /// The position on that the menu should be displayed at. public void ShowContextMenu(Control control, Point pos) { using var menu = new ContextMenu(); Populate(menu); var command = TrackPopupMenuEx(menu.Handle, TrackPopupMenuFlags.TPM_RETURNCMD, pos.X, pos.Y, m_MessageWindow.Handle); if (command > 0) { InvokeCommand((int)command - m_CmdFirst); } } private void InvokeCommand(int index) { var invoke = new CMINVOKECOMMANDINFOEX(index) { nShow = ShowWindowCommand.SW_SHOWNORMAL }; m_ComInterface2.InvokeCommand(invoke); } private void RemoveShellMenuItems(Menu menu) { const int tag = 0xAB; var menuInfo = new MENUINFO(); menuInfo.cbSize = (uint)Marshal.SizeOf(menuInfo); menuInfo.fMask = MenuInfoMember.MIM_MENUDATA; var itemInfo = new MENUITEMINFO(); itemInfo.cbSize = (uint)Marshal.SizeOf(itemInfo); itemInfo.fMask = MenuItemInfoMask.MIIM_ID | MenuItemInfoMask.MIIM_SUBMENU; // First, tag the managed menu items with an arbitary value (0xAB). TagManagedMenuItems(menu, tag); var remove = new List(); var count = GetMenuItemCount(menu.Handle); for (uint n = 0; n < count; ++n) { GetMenuItemInfo(menu.Handle, n, true, ref itemInfo); if (itemInfo.hSubMenu.IsNull) { // If the item has no submenu we can't get the tag, so check its ID to determine if it was added by the shell. if (itemInfo.wID >= m_CmdFirst) remove.Add(n); } else { GetMenuInfo(itemInfo.hSubMenu, ref menuInfo); if ((int)menuInfo.dwMenuData != tag) remove.Add(n); } } // Remove the unmanaged menu items. remove.Reverse(); foreach (var position in remove) { DeleteMenu(menu.Handle, (uint)position, MenuFlags.MF_BYPOSITION); } } private void TagManagedMenuItems(Menu menu, int tag) { var info = new MENUINFO(); info.cbSize = (uint)Marshal.SizeOf(info); info.fMask = MenuInfoMember.MIM_MENUDATA; info.dwMenuData = (UIntPtr)tag; foreach (MenuItem item in menu.MenuItems) { SetMenuInfo(item.Handle, info); } } private class MessageWindow : Control { private readonly ShellContextMenu m_Parent; public MessageWindow(ShellContextMenu parent) => m_Parent = parent; protected override void WndProc(ref Message m) { if (!m_Parent.HandleMenuMessage(ref m)) { base.WndProc(ref m); } } } } } #endif