Loop through IContextMenu

2020-03-25 06:53发布

问题:

How do I loop through all items and sub items of a IContextMenu and list all available verbs? So far, I have this working code extracted from JCL:

function DisplayContextMenuPidl(const Handle: THandle; const Folder: IShellFolder; Item: PItemIdList; Pos: TPoint): Boolean;
var
  Cmd: Cardinal;
  ContextMenu: IContextMenu;
  ContextMenu2: IContextMenu2;
  Menu: HMENU;
  CommandInfo: TCMInvokeCommandInfo;
  CallbackWindow: THandle;
  vteste : string;
begin
  Result := False;
  if (Item = nil) or (Folder = nil) then
    Exit;
  Folder.GetUIObjectOf(Handle, 1, Item, IID_IContextMenu, nil,
    Pointer(ContextMenu));
  if ContextMenu <> nil then
  begin
    Menu := CreatePopupMenu;
    if Menu <> 0 then
    begin
      if Succeeded(ContextMenu.QueryContextMenu(Menu, 0, 1, $7FFF, CMF_EXPLORE)) then
      begin
        CallbackWindow := 0;
        if Succeeded(ContextMenu.QueryInterface(IContextMenu2, ContextMenu2)) then
        begin
          CallbackWindow := CreateMenuCallbackWnd(ContextMenu2);
        end;
        ClientToScreen(Handle, Pos);
        Cmd := Cardinal(TrackPopupMenu(Menu, TPM_LEFTALIGN or TPM_LEFTBUTTON or TPM_RIGHTBUTTON or TPM_RETURNCMD, Pos.X, Pos.Y, 0, CallbackWindow, nil));
        if Cmd <> 0 then
        begin
          ResetMemory(CommandInfo, SizeOf(CommandInfo));
          CommandInfo.cbSize := SizeOf(TCMInvokeCommandInfo);
          CommandInfo.hwnd := Handle;
          CommandInfo.lpVerb := MakeIntResourceA(Cmd - 1);
          CommandInfo.nShow := SW_SHOWNORMAL;
          Result := Succeeded(ContextMenu.InvokeCommand(CommandInfo));
        end;
        if CallbackWindow <> 0 then
          DestroyWindow(CallbackWindow);
      end;
      DestroyMenu(Menu);
    end;
  end;
end;

This code works fine and it shows the context menu. I need to adapt it so it can list (maybe a log file) all the menu and submenus verbs.

EDIT

To clarify lets assume I have this context menu:

I want to log something like this:

Item verb

open= open
properties= properties
send to= sendto
send to bluetooh= xxx

EDIT

If somebody has another way of getting the verbs or call a item by its display text I would also appreciate it.

回答1:

To enumerate the items of the context menu you can use the Windows Menu functions : GetMenuItemCount , GetMenuItemInfo, GetSubMenu.

using these functions i wrote this function

uses
JclShell,
ShlObj;

function DisplayContextMenuInfo( const Folder: IShellFolder; Item: PItemIdList; List :TStrings): Boolean;

  function GetMenuItemCaption(const hSubMenu: HMENU; const MenuId: Integer): string;
  var
    MenuItemInfo: TMenuItemInfo;
  begin
    MenuItemInfo.cbSize := SizeOf(MenuItemInfo);
    MenuItemInfo.fMask := MIIM_STRING;
    SetLength(Result, 1024*Sizeof(Char));
    MenuItemInfo.dwTypeData := PChar(Result);
    MenuItemInfo.cch        := Length(Result)-1;
    if not GetMenuItemInfo(hSubMenu, MenuId, False, MenuItemInfo) then
      RaiseLastOSError;
    SetLength(Result, MenuItemInfo.cch*Sizeof(Char));
  end;

  Procedure LogGetMenuInfo(Menu: HMENU);
  var
    i             : Integer;
    ItemsCount    : Integer;
    MenuId        : Cardinal;
    Caption       : string;
  begin
     ItemsCount:=GetMenuItemCount(Menu);

     List.Add(Format('Number of items %d ',[ItemsCount]));
      for i:= 0 to ItemsCount - 1 do
      begin
        MenuId:=GetMenuItemID(Menu,i);

        case MenuId of

         Cardinal(-1) : begin
                          List.Add('');
                          List.Add(Format('Sub Menu',[]));
                          LogGetMenuInfo(GetSubMenu(Menu,i));
                        end;
         0            :

         else
                        begin
                          Caption:=GetMenuItemCaption(Menu, MenuId);
                          List.Add(Format('MenuId (Cmd) %d Caption %s  ',[MenuId,Caption]))
                        end;

        end;
      end;

  end;


var
  ContextMenu: IContextMenu;
  Menu: HMENU;

begin
  Result := False;
  if (Item = nil) or (Folder = nil) then  Exit;
  Folder.GetUIObjectOf(0, 1, Item, IID_IContextMenu, nil,  Pointer(ContextMenu));
  if ContextMenu <> nil then
  begin
    Menu := CreatePopupMenu;
    try
      if Menu <> 0 then
        if Succeeded(ContextMenu.QueryContextMenu(Menu, 0, 1, $7FFF, CMF_EXPLORE)) then
           LogGetMenuInfo(Menu);
    finally
      DestroyMenu(Menu);
    end;
  end;
end;

and call in this way

var
  ItemIdList: PItemIdList;
  Folder    : IShellFolder;
  FileName  : string;
begin
  FileName:= 'C:\Users\Dexter\Downloads\VirtualTreeview.pdf';
  ItemIdList := PathToPidlBind(FileName, Folder);
  if ItemIdList <> nil then
  begin
    DisplayContextMenuInfo( Folder, ItemIdList, Memo1.lines);
    PidlFree(ItemIdList);
  end;
end;

this will fill the TStrings passed as parameter with info like this :

Number of items 26 
MenuId (Cmd) 141 Caption Open with Adobe Reader X
MenuId (Cmd) 142 Caption &Open
MenuId (Cmd) 143 Caption &Print
MenuId (Cmd) 146 Caption Run &Sandboxed
MenuId (Cmd) 140 Caption Analizar con &AVG
Sub Menu
Number of items 28 
MenuId (Cmd) 105 Caption Add To Send To
MenuId (Cmd) 106 Caption Add To Templates
MenuId (Cmd) 107 Caption Change Date && Time
MenuId (Cmd) 108 Caption Change Extension: pdf
MenuId (Cmd) 109 Caption Choose Program
MenuId (Cmd) 110 Caption Command Prompt
MenuId (Cmd) 111 Caption Copy/Move To Folder
MenuId (Cmd) 112 Caption Copy Path
MenuId (Cmd) 113 Caption Delete On Reboot
MenuId (Cmd) 114 Caption Duplicate File
MenuId (Cmd) 115 Caption Encrypt File
MenuId (Cmd) 116 Caption Explore Rooted
MenuId (Cmd) 117 Caption Extended Delete
MenuId (Cmd) 118 Caption Extended Search && Replace     

now using this function you can pass the menuid (cmd) which you want execute

function InvokeContextMenuCommand(const Comnand: Cardinal; const Folder: IShellFolder; Item: PItemIdList): Boolean;
var
  ContextMenu   : IContextMenu;
  CommandInfo   : TCMInvokeCommandInfo;
  Menu          : HMENU;
  CallbackWindow: THandle;
begin
  Result := False;
  if Comnand=0 then exit;

  if (Item = nil) or (Folder = nil) then  Exit;
    Folder.GetUIObjectOf(0, 1, Item, IID_IContextMenu, nil,  Pointer(ContextMenu));
  if ContextMenu <> nil then
   begin
    Menu := CreatePopupMenu;
    try
      if Menu <> 0 then
      if Succeeded(ContextMenu.QueryContextMenu(Menu, 0, 1, $7FFF, CMF_EXPLORE)) then
      begin
        CallbackWindow:=0;
        TrackPopupMenu(Menu, TPM_LEFTALIGN or TPM_LEFTBUTTON or  TPM_RIGHTBUTTON or TPM_RETURNCMD, 0, 0, 0, CallbackWindow, nil);
        ZeroMemory(@CommandInfo, SizeOf(CommandInfo));
        CommandInfo.cbSize := SizeOf(TCMInvokeCommandInfo);
        CommandInfo.hwnd   := 0;
        CommandInfo.lpVerb := MakeIntResourceA(Comnand - 1);
        CommandInfo.nShow  := SW_SHOWNORMAL;
        Result := Succeeded(ContextMenu.InvokeCommand(CommandInfo));
      end;
    finally
     DestroyMenu(Menu);
    end;
   end;
end;

call in this way

var
  ItemIdList: PItemIdList;
  Folder    : IShellFolder;
  FileName  : string;
begin
  FileName:= 'C:\Users\Dexter\Downloads\VirtualTreeview.pdf';
  ItemIdList := PathToPidlBind(FileName, Folder);
  if ItemIdList <> nil then
  begin
    //calling the 141 Menuid = `Open with Adobe Reader X`
    InvokeContextMenuCommand(141,Folder, ItemIdList);
    PidlFree(ItemIdList);
  end;
end;


回答2:

After you call QueryContextMenu your menu will be mostly populated. You know your menu's handle, so can iterate its items and get the information you need.

function DisplayContextMenuPidl(const Handle: THandle; const Folder: IShellFolder; Item: PItemIdList; Pos: TPoint): Boolean;

//++
  procedure RecurseItems(const Menu: HMENU; Strings: TStrings; Indent: Integer = 0);

    function GetItemString(Parent: HMENU; Item: Integer): string;
    begin
      SetLength(Result, GetMenuString(Parent, Item, nil, 0, MF_BYPOSITION) + 1);
      GetMenuString(Parent, Item, PChar(Result), Length(Result), MF_BYPOSITION);
    end;

  var
    i: Integer;
    ItemInfo: TMenuItemInfo;
  begin
    for i := 0 to GetMenuItemCount(Menu) - 1 do begin
      FillChar(ItemInfo, SizeOf(ItemInfo), 0);
      ItemInfo.cbSize := SizeOf(ItemInfo);
      ItemInfo.fMask := MIIM_SUBMENU or MIIM_TYPE;
      GetMenuItemInfo(Menu, i, True, ItemInfo);
      if ItemInfo.fType <> MFT_SEPARATOR then
        Strings.Add(StringOfChar('-', Indent * 2) + GetItemString(Menu, i));
      if ItemInfo.hSubMenu <> 0 then
        RecurseItems(ItemInfo.hSubMenu, Strings, Indent + 1);
    end;
  end;
//--

var
  Cmd: Cardinal;
  ContextMenu: IContextMenu;
  ContextMenu2: IContextMenu2;
  ...

  begin
    Menu := CreatePopupMenu;
    if Menu <> 0 then
    begin
      if Succeeded(ContextMenu.QueryContextMenu(Menu, 0, 1, $7FFF, CMF_EXPLORE)) then

//++
      Memo1.Clear;
      RecurseItems(Menu, Memo1.Lines);
//--

      begin
        CallbackWindow := 0;
      ..


It won't really make any difference if you get items' text after you retrieve an 'IContextMenu2' interface or not, because sub menus like 'Send To' or 'New' are not populated until their parent menu item is selected. There's no way in that routine you'll have access to them. Note below the two items that have failed to expand in the sample output of the above code:

&Open
Run as &administrator
Troubleshoot compatibilit&y
7-Zip
--Open archive
--Extract files...
--Extract Here
--Test archive
--Add to archive...
S&hare with
--
Pin to Tas&kbar
Pin to Start Men&u
Restore previous &versions
Se&nd to
--
Cu&t
&Copy
Create &shortcut
&Delete
P&roperties


Messages to show the sub-items will be passing through your CallbackWindow's WndProc, like WM_INITMENUPOPUP, WM_ENTERIDLE, WM_MEASUREITEM, WM_DRAWITEM. But I don't think trying to extract the information there would make any sense at all..