diff --git a/Windows.Shell/ShellObjects/ShellContextMenu.cs b/Windows.Shell/ShellObjects/ShellContextMenu.cs index 5c949d39..75aa5e93 100644 --- a/Windows.Shell/ShellObjects/ShellContextMenu.cs +++ b/Windows.Shell/ShellObjects/ShellContextMenu.cs @@ -89,7 +89,7 @@ namespace Vanara.Windows.Shell } /// Gets the underlying COM interface. - public IContextMenu ComInterface { get; set; } + public IContextMenu ComInterface { get; private set; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() @@ -99,6 +99,30 @@ namespace Vanara.Windows.Shell 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 IconLocation GetIconLocationForCommand(int command) => IconLocation.TryParse(GetCommandString(command, GCS.GCS_VERBICONW), out var l) ? l : null; + + /// 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); + } + /// Handles context menu messages when the is displayed on a Form's main menu bar. /// /// @@ -149,6 +173,82 @@ namespace Vanara.Windows.Shell 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. + /// + 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) + { + 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; + } + ComInterface.InvokeCommand(invoke); + } + /// Invokes the Copy command on the shell item(s). public void InvokeCopy() => InvokeVerb("copy"); @@ -182,15 +282,25 @@ namespace Vanara.Windows.Shell /// Invokes the specified verb on the shell item(s). /// The verb to invoke. /// Flags that specify how to display any opened window. - public void InvokeVerb(string verb, ShowWindowCommand show = ShowWindowCommand.SW_NORMAL) + /// + /// 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) { - var invoke = new CMINVOKECOMMANDINFOEX - { - cbSize = (uint)Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX)), - lpVerb = new SafeResourceId(verb, CharSet.Ansi), - nShow = show - }; - ComInterface.InvokeCommand(invoke); + 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. @@ -212,37 +322,26 @@ namespace Vanara.Windows.Shell } } - private void InvokeCommand(int index) + private string GetCommandString(ResourceId command, GCS stringType) { - var invoke = new CMINVOKECOMMANDINFOEX(index) { nShow = ShowWindowCommand.SW_SHOWNORMAL }; - m_ComInterface2.InvokeCommand(invoke); + using var mStr = new SafeCoTaskMemString(4096); + try { ComInterface.GetCommandString(command, stringType, default, mStr, mStr.Size / 2U); } + catch { return null; } + return mStr.ToString(); } #if !NET5_0 /// Populates a with the context menu items for a shell item. /// The menu to populate. - /// The flags to pass to . + /// The flags to pass to . /// /// If this method is being used to populate a Form's main menu then you need to call in the Form's /// message handler. /// - public void Populate(Menu menu, CMF menuFlags = CMF.CMF_NORMAL) + public void Populate(Menu menu, CMF menuOptions = CMF.CMF_NORMAL) { RemoveShellMenuItems(menu); - ComInterface.QueryContextMenu(menu.Handle, 0, m_CmdFirst, int.MaxValue, menuFlags); - } - - /// Shows a context menu for a shell item. - /// The position on the screen that the menu should be displayed at. - public void ShowContextMenu(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); - } + ComInterface.QueryContextMenu(menu.Handle, 0, m_CmdFirst, int.MaxValue, menuOptions); } private void RemoveShellMenuItems(Menu menu) @@ -260,7 +359,7 @@ namespace Vanara.Windows.Shell // First, tag the managed menu items with an arbitary value (0xAB). TagManagedMenuItems(menu, tag); - var remove = new List(); + var remove = new Stack(); var count = GetMenuItemCount(menu.Handle); for (uint n = 0; n < count; ++n) { @@ -269,37 +368,172 @@ namespace Vanara.Windows.Shell 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); + if (itemInfo.wID >= m_CmdFirst) remove.Push(n); } else { GetMenuInfo(itemInfo.hSubMenu, ref menuInfo); - if ((int)menuInfo.dwMenuData != tag) remove.Add(n); + if ((int)menuInfo.dwMenuData != tag) remove.Push(n); } } // Remove the unmanaged menu items. - remove.Reverse(); - foreach (var position in remove) - { - DeleteMenu(menu.Handle, (uint)position, MenuFlags.MF_BYPOSITION); - } + while (remove.Count > 0) + DeleteMenu(menu.Handle, remove.Pop(), 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; + var info = new MENUINFO + { + cbSize = (uint)Marshal.SizeOf(typeof(MENUINFO)), + fMask = MenuInfoMember.MIM_MENUDATA, + dwMenuData = (UIntPtr)tag + }; - foreach (MenuItem item in menu.MenuItems) + 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) + { + 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); + 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); + } + + /// + /// 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; } + + /// Gets the icon location associated with the menu's image. + public IconLocation IconLocation { 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; } + + /// 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); + if (scm != null) + { + SubMenus[i].Verb = scm.GetVerbForCommand(SubMenus[i].Id); + SubMenus[i].HelpText = scm.GetHelpTextForCommand(SubMenus[i].Id); + SubMenus[i].IconLocation = scm.GetIconLocationForCommand(SubMenus[i].Id); + } + } + return SubMenus; + } + } + private class MessageWindow : Control { private readonly ShellContextMenu m_Parent;