// Credit due to Gong-Shell from which this was largely taken.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Vanara.Extensions;
using Vanara.InteropServices;
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 Populate or methods 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 : IDisposable
{
internal const int m_CmdFirst = 0x8000;
private readonly IContextMenu2 m_ComInterface2;
private readonly IContextMenu3 m_ComInterface3;
private readonly BasicMessageWindow m_MessageWindow;
private bool disposedValue;
static ShellContextMenu() => Ole32.OleInitialize(default); // Not sure why necessary, but it fails without
/// Initialises a new instance of the class.
/// The items to which the context menu should refer.
public ShellContextMenu(params ShellItem[] items)
{
if (items is null)
throw new ArgumentNullException(nameof(items));
if (items.Length == 1 && items[0].IsFolder)
{
var isf = items[0] is ShellFolder sf ? sf.IShellFolder : items[0].IShellItem.BindToHandler(null, BHID.BHID_SFObject.Guid());
ComInterface = isf.CreateViewObject(HWND.NULL);
}
else
{
if (Array.IndexOf(items, ShellFolder.Desktop) != -1)
throw new Exception("If the desktop folder is specified, it must be the only item.");
var pidls = new IntPtr[items.Length];
ShellFolder parent = null;
for (var n = 0; n < items.Length; ++n)
{
if (n == 0)
parent = items[n].Parent;
else if (items[n].Parent != parent)
throw new Exception("All shell items must have the same parent");
pidls[n] = (IntPtr)items[n].PIDL.LastId;
}
ComInterface = parent.IShellFolder.GetUIObjectOf(HWND.NULL, pidls);
}
m_ComInterface2 = ComInterface as IContextMenu2;
m_ComInterface3 = ComInterface as IContextMenu3;
m_MessageWindow = new BasicMessageWindow(WindowMessageFilter);
}
/// Finalizes an instance of the class.
~ShellContextMenu()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
/// Gets the underlying COM interface.
public IContextMenu ComInterface { get; private set; }
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
System.GC.SuppressFinalize(this);
}
/// Gets the help text for a specified command.
/// The menu command identifier offset.
/// The help text value if available; otherwise .
public string GetHelpTextForCommand(int command) => GetCommandString(command, GCS.GCS_HELPTEXTW);
/// Gets the icon location for a specified command.
/// The menu command identifier offset.
/// The icon location if available; otherwise .
public string GetVerbIconLocationForCommand(int command) => GetCommandString(command, GCS.GCS_VERBICONW);
/// Gets the verb for a specified command.
/// The menu command identifier offset.
/// The verb if available; otherwise .
public string GetVerbForCommand(int command) => GetCommandString(command, GCS.GCS_VERBW);
/// Gets the information of all the menu items supported by the underlying interface.
/// The menu item information.
public MenuItemInfo[] GetItems(CMF menuOptions = CMF.CMF_EXTENDEDVERBS)
{
using var hmenu = CreatePopupMenu();
ComInterface.QueryContextMenu(hmenu, 0, m_CmdFirst, int.MaxValue, menuOptions).ThrowIfFailed();
return MenuItemInfo.GetMenuItems(hmenu, this);
}
private bool WindowMessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn)
{
lReturn = default;
try
{
if ((msg == (uint)WindowMessage.WM_COMMAND) && ((int)wParam >= m_CmdFirst))
{
InvokeCommand((int)wParam - m_CmdFirst);
return true;
}
else
{
if (m_ComInterface3 is not null)
{
if (m_ComInterface3.HandleMenuMsg2(msg, wParam, lParam, out var lRet).Succeeded)
lReturn = lRet;
return true;
}
else if (m_ComInterface2 is not null)
{
if (m_ComInterface2.HandleMenuMsg(msg, wParam, lParam).Succeeded)
lReturn = default;
return true;
}
}
}
catch { }
return false;
}
/// Invokes the command.
///
/// The address of a null-terminated string that specifies the language-independent name of the command to carry out. This member is
/// typically a string when a command is being activated by an application. The system provides predefined constant values for the
/// following command strings.
///
/// If a canonical verb exists and a menu handler does not implement the canonical verb, it must return a failure code to enable the
/// next handler to be able to handle this verb.Failing to do this will break functionality in the system including ShellExecute.
///
///
/// Alternatively, rather than a pointer, this parameter can be MAKEINTRESOURCE(offset) where offset is the menu-identifier offset
/// of the command to carry out. Implementations can use the IS_INTRESOURCE macro to detect that this alternative is being employed.
/// The Shell uses this alternative when the user chooses a menu command.
///
///
/// A set of values to pass to the ShowWindow function if the command displays a window or starts an application.
///
/// A handle to the window that is the owner of the shortcut menu. An extension can also use this handle as the owner of any message
/// boxes or dialog boxes it displays. Callers must specify a legitimate HWND that can be used as the owner window for any UI that
/// may be displayed. Failing to specify an HWND when calling from a UI thread (one with windows already created) will result in
/// reentrancy and possible bugs in the implementation of this call.
///
/// If supplied, the point where the command is invoked.
///
/// The implementation can spin off a new thread or process to handle the call and does not need to block on completion of the
/// function being invoked. For example, if the verb is "delete" the call may return before all of the items have been deleted.
/// Since this is advisory, calling applications that specify this flag cannot guarantee that this request will be honored if they
/// are not familiar with the implementation of the verb that they are invoking.
///
///
/// If , the SHIFT key is pressed. Use this instead of polling the current state of the keyboard that may have
/// changed since the verb was invoked.
///
///
/// If , the CTRL key is pressed. Use this instead of polling the current state of the keyboard that may have
/// changed since the verb was invoked..
///
/// An optional keyboard shortcut to assign to any application activated by the command.
///
/// If , indicates that the method might want to keep track of the item being invoked for features like the
/// "Recent documents" menu.
///
///
/// Do not perform a zone check. This flag allows ShellExecuteEx to bypass zone checking put into place by IAttachmentExecute.
///
/// Optional parameters.
public void InvokeCommand(ResourceId verb, ShowWindowCommand show = ShowWindowCommand.SW_SHOWNORMAL, HWND parent = default,
Point? location = default, bool allowAsync = false, bool shiftDown = false, bool ctrlDown = false, uint hotkey = 0,
bool logUsage = false, bool noZoneChecks = false, string parameters = null)
{
var invoke = new CMINVOKECOMMANDINFOEX
{
cbSize = (uint)Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX)),
hwnd = parent,
fMask = (parent.IsNull ? CMIC.CMIC_MASK_FLAG_NO_UI : 0) | (hotkey != 0 ? CMIC.CMIC_MASK_HOTKEY : 0),
lpVerb = verb,
nShow = show,
dwHotKey = hotkey,
};
if (allowAsync) invoke.fMask |= CMIC.CMIC_MASK_ASYNCOK;
if (shiftDown) invoke.fMask |= CMIC.CMIC_MASK_SHIFT_DOWN;
if (ctrlDown) invoke.fMask |= CMIC.CMIC_MASK_CONTROL_DOWN;
if (logUsage) invoke.fMask |= CMIC.CMIC_MASK_FLAG_LOG_USAGE;
if (noZoneChecks) invoke.fMask |= CMIC.CMIC_MASK_NOZONECHECKS;
if (location.HasValue)
{
invoke.ptInvoke = location.Value;
invoke.fMask |= CMIC.CMIC_MASK_PTINVOKE;
}
if (!verb.IsIntResource)
{
invoke.lpVerbW = (string)verb;
invoke.fMask |= CMIC.CMIC_MASK_UNICODE;
}
if (parameters != null)
{
invoke.lpParameters = invoke.lpParametersW = parameters;
invoke.fMask |= CMIC.CMIC_MASK_UNICODE;
}
ComInterface.InvokeCommand(invoke);
}
/// 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).
/// The verb to invoke.
/// Flags that specify how to display any opened window.
///
/// A handle to the window that is the owner of the shortcut menu. An extension can also use this handle as the owner of any message
/// boxes or dialog boxes it displays. Callers must specify a legitimate HWND that can be used as the owner window for any UI that
/// may be displayed. Failing to specify an HWND when calling from a UI thread (one with windows already created) will result in
/// reentrancy and possible bugs in the implementation of this call.
///
public void InvokeVerb(string verb, [Optional] ShowWindowCommand show, [Optional] HWND parent) =>
InvokeCommand(new SafeResourceId(verb), show, parent);
/// Shows a context menu for a shell item.
/// The position on the screen that the menu should be displayed at.
/// The options that determine which items are requested from .
public void ShowContextMenu(Point pos, CMF menuOptions = CMF.CMF_EXTENDEDVERBS)
{
using var hmenu = CreatePopupMenu();
ComInterface.QueryContextMenu(hmenu, 0, m_CmdFirst, int.MaxValue, menuOptions).ThrowIfFailed();
var command = TrackPopupMenuEx(hmenu, TrackPopupMenuFlags.TPM_RETURNCMD, pos.X, pos.Y, m_MessageWindow.Handle);
if (command > 0)
InvokeCommand((int)command - m_CmdFirst);
}
/// Releases unmanaged and - optionally - managed resources.
///
/// to release both managed and unmanaged resources; to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
}
m_MessageWindow?.Dispose();
ComInterface = null;
disposedValue = true;
}
}
private string GetCommandString(int command, GCS stringType)
{
using var mStr = new SafeCoTaskMemString(4096);
return ComInterface.GetCommandString((IntPtr)command, stringType, default, mStr, (uint)mStr.Capacity).Succeeded ? mStr : null;
}
#if !NET5_0 && !NETCOREAPP3_1
/// Populates a with the context menu items for a shell item.
/// The menu to populate.
/// The flags to pass to .
public void Populate(Menu menu, CMF menuOptions = CMF.CMF_NORMAL)
{
RemoveShellMenuItems(menu);
ComInterface.QueryContextMenu(menu.Handle, 0, m_CmdFirst, int.MaxValue, menuOptions);
}
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 Stack();
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.Push(n);
}
else
{
GetMenuInfo(itemInfo.hSubMenu, ref menuInfo);
if ((int)menuInfo.dwMenuData != tag) remove.Push(n);
}
}
// Remove the unmanaged menu items.
while (remove.Count > 0)
DeleteMenu(menu.Handle, remove.Pop(), MenuFlags.MF_BYPOSITION);
}
private void TagManagedMenuItems(Menu menu, int tag)
{
var info = new MENUINFO
{
cbSize = (uint)Marshal.SizeOf(typeof(MENUINFO)),
fMask = MenuInfoMember.MIM_MENUDATA,
dwMenuData = (IntPtr)tag
};
foreach (Menu item in menu.MenuItems)
{
SetMenuInfo(item.Handle, info);
}
}
#endif
/// Provides information about a single menu entry discovered in a native menu.
public class MenuItemInfo
{
internal MenuItemInfo(HMENU hMenu, uint idx, ShellContextMenu scm)
{
using var strmem = new SafeHGlobalHandle(512);
var mii = new MENUITEMINFO
{
cbSize = (uint)Marshal.SizeOf(typeof(MENUITEMINFO)),
fMask = MenuItemInfoMask.MIIM_ID | MenuItemInfoMask.MIIM_SUBMENU | MenuItemInfoMask.MIIM_FTYPE | MenuItemInfoMask.MIIM_STRING | MenuItemInfoMask.MIIM_STATE | MenuItemInfoMask.MIIM_BITMAP,
fType = MenuItemType.MFT_STRING,
dwTypeData = (IntPtr)strmem,
cch = strmem.Size / (uint)StringHelper.GetCharSize()
};
Win32Error.ThrowLastErrorIfFalse(GetMenuItemInfo(hMenu, idx, true, ref mii));
Id = unchecked((int)(mii.wID - m_CmdFirst));
Text = mii.fType.IsFlagSet(MenuItemType.MFT_SEPARATOR) ? "-" : mii.fType.IsFlagSet(MenuItemType.MFT_STRING) ? strmem.ToString(-1, CharSet.Auto) : "";
Type = mii.fType;
State = mii.fState;
BitmapHandle = mii.hbmpItem;
SubMenus = GetMenuItems(mii.hSubMenu, scm);
}
///
/// A handle to the bitmap to be displayed, or it can be one of the values in the following table.
///
///
/// Value
/// Meaning
///
/// -
/// HBMMENU_CALLBACK ((HBITMAP) -1)
///
/// A bitmap that is drawn by the window that owns the menu. The application must process the WM_MEASUREITEM and WM_DRAWITEM messages.
///
///
/// -
/// HBMMENU_MBAR_CLOSE ((HBITMAP) 5)
/// Close button for the menu bar.
///
/// -
/// HBMMENU_MBAR_CLOSE_D ((HBITMAP) 6)
/// Disabled close button for the menu bar.
///
/// -
/// HBMMENU_MBAR_MINIMIZE ((HBITMAP) 3)
/// Minimize button for the menu bar.
///
/// -
/// HBMMENU_MBAR_MINIMIZE_D ((HBITMAP) 7)
/// Disabled minimize button for the menu bar.
///
/// -
/// HBMMENU_MBAR_RESTORE ((HBITMAP) 2)
/// Restore button for the menu bar.
///
/// -
/// HBMMENU_POPUP_CLOSE ((HBITMAP) 8)
/// Close button for the submenu.
///
/// -
/// HBMMENU_POPUP_MAXIMIZE ((HBITMAP) 10)
/// Maximize button for the submenu.
///
/// -
/// HBMMENU_POPUP_MINIMIZE ((HBITMAP) 11)
/// Minimize button for the submenu.
///
/// -
/// HBMMENU_POPUP_RESTORE ((HBITMAP) 9)
/// Restore button for the submenu.
///
/// -
/// HBMMENU_SYSTEM ((HBITMAP) 1)
/// Windows icon or the icon of the window specified in dwItemData.
///
///
///
public HBITMAP BitmapHandle { get; }
/// Gets the help text (tool tip) associated with the menu.
public string HelpText { get; internal set; }
/// An application-defined value that identifies the menu item.
public int Id { get; }
/// The menu item state. This member can be one or more of the values.
public MenuItemState State { get; }
///
/// The submenu items associated with the menu item. If the menu item is not an item that opens a drop-down menu or submenu,
/// this member has no values.
///
public MenuItemInfo[] SubMenus { get; }
/// The contents of the menu item. The meaning of this member depends on the value of .
public string Text { get; }
///
/// The menu item type. This member can be one or more of the values.
/// The MFT_BITMAP, MFT_SEPARATOR, and MFT_STRING values cannot be combined with one another.
///
public MenuItemType Type { get; }
/// Gets the verb associated with the menu.
public string Verb { get; internal set; }
/// Gets the icon location associated with the menu's image.
public string VerbIconLocation { get; internal set; }
/// Recursively gets the information for all menu item entries supplied by the provided native menu.
/// The handle to the created native menu.
/// An array of instances with information about the entries in .
public static MenuItemInfo[] GetMenuItems(HMENU hMenu) => GetMenuItems(hMenu, null);
internal static MenuItemInfo[] GetMenuItems(HMENU hMenu, ShellContextMenu scm)
{
if (hMenu.IsNull)
return new MenuItemInfo[0];
var SubMenus = new MenuItemInfo[GetMenuItemCount(hMenu)];
for (uint i = 0; i < SubMenus.Length; i++)
{
SubMenus[i] = new MenuItemInfo(hMenu, i, scm);
System.Diagnostics.Debug.WriteLine($"Processing submenu {i} ({SubMenus[i].Text})");
if (scm != null && SubMenus[i].Type == MenuItemType.MFT_STRING)
{
var id = SubMenus[i].Id;
SubMenus[i].Verb = scm.GetVerbForCommand(id);
SubMenus[i].HelpText = scm.GetHelpTextForCommand(id);
SubMenus[i].VerbIconLocation = scm.GetVerbIconLocationForCommand(id);
}
}
return SubMenus;
}
}
}
}