如何访问Windows shell上下文菜单项?

12
在Windows资源管理器中,右键单击文件会弹出一个上下文菜单,其中包含内置项目(例如“发送到…”)和/或第三方操作(如“使用WinZip压缩文件”)。我的问题是:
  • 如何获取特定文件的所有可用菜单项的完整列表?
  • 对于每个菜单项,如何获取标题?
  • 如何调用特定磁盘文件的特定菜单项操作?
谢谢!
[编辑]:虽然其他信息绝对有用,但Delphi解决方案将非常感激!

你想创建自己的上下文菜单项还是操作现有的菜单项? - Liton
@Liton,我不是在创建自己的 shell 上下文菜单项,而是要操作现有的内置或第三方菜单项。 - Edwin Yip
3个回答

9
获取Shell上下文菜单的关键是使用IContextMenu接口。

查看这篇很棒的文章Shell context menu support获取更多细节。

更新

对于Delphi示例,您可以查看JEDI JCL的JclShell单元(检查DisplayContextMenu函数)和Delphi的示例文件夹中包含的ShellCtrls单元。


谢谢您提供的链接,我会去查看一下! - Edwin Yip
嗨 RRUZ,JCL 软件包中的 DisplayContextMenu 函数完全符合我的要求!谢谢! - Edwin Yip

8

简短回答

尝试使用JAM Software的ShellBrowser Components。他们提供了一个组件,可以让您显示Explorer上下文菜单,并混合自己的命令从TPopupMenu。


长篇回答

获取资源管理器菜单,查询其所有属性并将它们托管在您自己的菜单中是可能的,但您真的应该熟悉阅读/编写低级Win32代码,并具有C语言的工作知识将会有所帮助。您还需要注意一些细节(gotchas)(下面有说明)。我强烈推荐阅读 Raymond Chen 的 如何托管 IContextMenu 系列文章,以了解更多技术细节。

更加容易的方法是查询 IContextMenu 接口,然后获取 HMENU,使用 TrackPopupMenu 让 Windows 显示菜单,最后调用 InvokeCommand。

以下部分代码未经测试或已经修改,因此请自行决定是否使用。

以下是获取IContextMenu的方法,针对基本文件夹内的一组文件:

function GetExplorerMenu(AHandle: HWND; const APath: string;
  AFilenames: TStrings): IContextMenu;
var
  Desktop, Parent: IShellFolder;
  FolderPidl: PItemIDList;
  FilePidls: array of PItemIDList;
  PathW: WideString;
  i: Integer;
begin
  // Retrieve the Desktop's IShellFolder interface
  OleCheck(SHGetDesktopFolder(Desktop));
  // Retrieve the parent folder's PItemIDList and then it's IShellFolder interface
  PathW := WideString(IncludeTrailingPathDelimiter(APath));
  OleCheck(Desktop.ParseDisplayName(AHandle, nil, PWideChar(PathW),
    Cardinal(nil^), FolderPidl, Cardinal(nil^)));
  try
    OleCheck(Desktop.BindToObject(FolderPidl, nil, IID_IShellFolder, Parent));
  finally
    SHFree(FolderPidl);
  end;
  // Retrieve PIDLs for each file, relative the the parent folder
  SetLength(FilePidls, AFilenames.Count);
  try
    FillChar(FilePidls[0], SizeOf(PItemIDList) * AFilenames.Count, 0);
    for i := 0 to AFilenames.Count-1 do begin
      PathW := WideString(AFilenames[i]);
      OleCheck(Parent.ParseDisplayName(AHandle, nil, PWideChar(PathW),
        Cardinal(nil^), FilePidls[i], Cardinal(nil^)));
    end;
    // Get the context menu for the files from the parent's IShellFolder
    OleCheck(Parent.GetUIObjectOf(AHandle, AFilenames.Count, FilePidls[0],
      IID_IContextMenu, nil, Result));
  finally
    for i := 0 to Length(FilePidls) - 1 do
      SHFree(FilePidls[i]);
  end;
end;

要获取实际菜单项,您需要调用IContextMenu.QueryContextMenu。您可以使用DestroyMenu销毁返回的HMENU。
function GetExplorerHMenu(const AContextMenu: IContextMenu): HMENU;
const
  MENUID_FIRST = 1;
  MENUID_LAST = $7FFF;
var
  OldMode: UINT;
begin
  OldMode := SetErrorMode(SEM_FAILCRITICALERRORS or SEM_NOOPENFILEERRORBOX);
  try
    Result := CreatePopupMenu;
    AContextMenu.QueryContextMenu(Result, 0, MENUID_FIRST, MENUID_LAST, CMF_NORMAL);
  finally
    SetErrorMode(OldMode);
  end;
end;

以下是如何实际调用用户从菜单中选择的命令:

procedure InvokeCommand(const AContextMenu: IContextMenu; AVerb: PChar);
const
  CMIC_MASK_SHIFT_DOWN   = $10000000;
  CMIC_MASK_CONTROL_DOWN = $20000000;
var
  CI: TCMInvokeCommandInfoEx;
begin
  FillChar(CI, SizeOf(TCMInvokeCommandInfoEx), 0);
  CI.cbSize := SizeOf(TCMInvokeCommandInfo);
  CI.hwnd := GetOwnerHandle(Owner);
  CI.lpVerb := AVerb;
  CI.nShow := SW_SHOWNORMAL;
  // Ignore return value for InvokeCommand.  Some shell extensions return errors
  // from it even if the command worked.
  try
    AContextMenu.InvokeCommand(PCMInvokeCommandInfo(@CI)^)
  except on E: Exception do
    MessageDlg(Owner, E.Message, mtError, [mbOk], 0);
  end;
end;

procedure InvokeCommand(const AContextMenu: IContextMenu; ACommandID: UINT);
begin
  InvokeCommand(AContextMenu, MakeIntResource(Word(ACommandID)));
end;

现在您可以使用GetMenuItemInfo函数获取标题、位图等,但更简单的方法是调用TrackPopupMenu并让Windows显示弹出菜单。代码如下:
procedure ShowExplorerMenu(AForm: TForm; AMousePos: TPoint; 
  const APath: string; AFilenames: TStrings; );
var
  ShellMenu: IContextMenu;
  Menu: HMENU;
  MenuID: LongInt;
begin
  ShellMenu := GetExplorerMenu(AForm.Handle, APath, AFilenames);
  Menu := GetExplorerHMenu(ShellMenu);
  try
    MenuID := TrackPopupMenu(Menu, TPM_LEFTALIGN or TPM_TOPALIGN or TPM_RETURNCMD, 
      AMousePos.X, AMousePos.Y, 0, AForm.Handle, nil);
    InvokeCommand(ShellMenu, MenuID - MENUID_FIRST);
  finally
    DestroyMenu(Menu);
  end;
end;

如果您想要提取菜单项/标题并将它们添加到自己的弹出菜单中(我们使用Toolbar 2000并且确实这样做),那么您将遇到以下主要问题:
  • "发送到"菜单和任何其他按需构建的菜单都不起作用,除非您处理消息并将其传递给IContextMenu2/IContextMenu3接口。
  • 菜单位图采用几种不同的格式。 Delphi无法处理Vista高色彩位图,除非进行协助,而旧版位图则使用XOR混合到背景色上。
  • 某些菜单项是所有者绘制的,因此您必须捕获绘制消息并让它们绘制到自己的画布上。
  • 提示字符串不起作用,除非您手动查询它们。
  • 您需要管理IContextMenu和HMENU的生命周期,并且只有在弹出菜单关闭后才能释放它们。

嗨,Craig,谢谢你提供详细的技术信息,它们非常有帮助!但是我不得不接受RRUZ的答案,因为他向我指出了JCL中的DisplayContextMenu函数,用一行代码实现了我想要的功能... - Edwin Yip

0
这是一个示例,展示了如何从 Delphi 应用程序中使用操作系统逻辑来打开默认的邮件客户端,并显示一个新的邮件,附带 (已选择的) 文件。具体地说,它演示了 "发送到 ... | 邮件收件人" 右键菜单项背后的操作系统逻辑。

如何使用 Delphi 模拟 “发送到 ...” ?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接