Explorer 上下文菜单管理器
辅助类,用于从您的应用程序中添加、修改和删除基于注册表的 Explorer 上下文菜单。
引言
添加Explorer 上下文菜单的任务通常留给安装程序来完成,因为当它们运行时,它们拥有访问注册表的所需管理员权限。然而,有时能够运行时修改它们也很好,例如作为用户选项添加或删除关联的扩展。
ExplorerMenuManager
是一个类,您可以使用它在运行时添加、删除或修改 Explorer 上下文菜单。它将帮助创建这些菜单,无论是针对当前用户还是整个系统。
它也是实现 Shell 扩展的更简单、更干净的替代方案。
背景
在准备另一篇关于Explorer Shell 扩展以及在 .NET 中使用托管代码的问题的文章时,我开始考虑简单的替代方案。我决定写一篇配套文章,而不是将其添加到那篇文章中。
当然,在处理注册表时,“简单”是一个相对的术语。ExplorerMenuManager
背后的想法是提供尽可能多的功能,并避免对 Shell 扩展的需求,但仍易于实现。
特别是,我希望能够动态地更改关联。也就是说,如果需要,应用程序可以解除一个或多个扩展及其相关上下文菜单的关联。
上下文菜单与 Shell 扩展
Shell 扩展是 DLL 文件,Explorer 会调用它们来获取有关上下文菜单的信息。这些在注册表中列为 shellex
键。菜单文本等内容从未写入注册表,而是由 DLL Shell 扩展中的方法提供。要执行的操作也由 Shell 扩展决定,它可以自行完成工作或启动其他进程。
相反,标准或静态上下文菜单使用 shell
键,菜单文本和命令存储在注册表中。这使得它们易于查找和删除,如果您发现它们很烦人。
从最终用户的角度来看,它们基本上做同样的事情,但有一些区别。
单击具有多个选定文件的标准上下文菜单的菜单命令,将启动目标应用程序的多个实例,每个选定文件一个。没有办法将多个文件发送到一个实例(但VB 可以非常轻松地克服这一点——见下文)。使用进程内的 Shell 扩展启动器,所有选定的文件一开始都会被发送到一个实例。
另一个细微的区别是,当选择多个文件时,标准菜单仅在所有选定文件都注册为同一类型时显示。Shell 扩展菜单是根据右键单击的文件显示的,并且可以向 Shell 扩展发送/处理混合类型的文件。
从技术上讲,它们有很大不同。Shell 扩展作为 Explorer 的一部分运行,并且可以是除了上下文菜单处理程序以外的任何东西。它们可以是属性页扩展或缩略图处理程序等。由于它们在进程内运行,用 .NET 编写的 Shell 扩展可能会消耗更多内存,并且可能明显变慢。有几条微软的警告关于 .NET Shell 扩展,所以它们有时可能会导致问题或失败(我发现它们明显变慢)。
本文将介绍如何使用 ExplorerMenuManager
按用户添加、修改或删除上下文菜单关联。ExplorerMenuManager
提供了在整个系统(HKCR)或按用户安装和修改它们的手段,尽管后者不被推荐。
它还将展示如何在代码中响应以获取从稍后启动的实例获得的命令行,以便您的上下文菜单可以像 Shell 扩展一样将多个文件发送到一个实例。
Using the Code
ExplorerMenuManager
是您用来在注册表中定义和实现上下文菜单的类。它支持添加和删除菜单项,这样您就可以允许用户勾选他们想与您的应用程序关联的扩展。它非常易于使用。
Dim ExpMenu As New ExpMenuManager
With ExpMenu
.AddExtensions("zig", "zag", ".zork") ' or collect from UI (see demo)
.MenuText = "Open in ZigZag"
.ImageFile = "zigzag.ico"
.ExtendedMenu = False
.Remove = False ' adding these not removing them
End With
- 您指定的任何图标都必须存在于与您的 EXE 相同的文件夹中。
- 图像必须是有效的 16x16 图标。
各种方法和工具旨在将您定义的菜单与活动 EXE 关联起来。因此,它会自动确定应用程序名称等。要在 VS IDE 中运行和测试,仍然需要在目标目录中包含编译后的版本以及图标等。稍后将介绍如何配置 VS 以此目的。
上下文菜单通常是系统范围的,即将“.txt”与记事本关联会为所有用户都这样做。由于这需要写入 HKCR,而 HKCR 又需要管理员权限,因此 ExplorerMenuManager
将创建 RegEdit
所需的指令,然后选择性地以提升的进程启动它。
If ExpMenu.CreateRegFileText(True) Then ' creates the text/script
ExpMenu.RegisterExtensions() ' start regedit for you
End If
结果将是一个新创建的上下文菜单条目,并且可以立即使用。
zigfile
shell
Open with ZigZag
commmand "...pathto\MyMainApp.exe" "%1"
CreateRegFileText
创建 RegEdit
所需的文本,RegisterExtensions
根据该文本创建一个临时文件,然后尝试以提升的进程运行RegEdit
。如果 ExpMenu.Remove
为 True
,则会删除扩展关联而不是添加到注册表中。
您还可以使用 RegEditText
属性的内容自行调用RegEdit
,也许是为了让用户在继续之前查看内容。在这种情况下,您可以复制 RegisterExtensions
中发生的操作,而是使用 RegisterByFile
。
' get a temp file... or bug the user with a FileDialog
Dim RegFile As String = Path.GetTempFileName()
ExpMenu.CreateRegFileText(True) ' create the RegEdit file contents
' ToDo: let the user approve changes (?)
' save data to file
File.WriteAllLines(RegFile, ExpMenu.RegEditText) ' save script to file
' start regedit as process... or do it yourself
ExpMenu.RegisterByFile(RegFile, True)
File.Delete(RegFile)
当然,在写入要馈送给 RegEdit
的文件时,应格外小心,特别是如果用户“帮忙”的话。
添加/删除关联
在修改关联扩展的情况下,这是一个两步过程:首先,需要删除所有旧扩展,然后添加新集。即使两个集合之间很可能存在大量重叠,这样做也更简单、更可靠。
ExplorerMenuManager
提供了将第二个指令集附加到第一个指令集的方法,以避免两次启动RegEdit
以及所需的 UAC 对话框。首先删除所有可能扩展的关联。
Dim ExpMenu As New ContextMenuManager
With ExpMenu
.AddExtensions(AllPossibleFileExts)
.MenuText = ExpMenuText
.ImageFile = ...
.ExtendedMenu = False
.Remove = Remove
End With
ExpMenu.CreateRegFileText(True) ' create the remove instructions
ExpMenu.RemoveExtension("mp4") ' remove undesired extension
ExpMenu.Remove = False ' change to ADD mode
ExpMenu.AppendRegFileText() ' Append ADD instructions to existing REMOVE set
ExpMenu.RegisterExtensions() ' Exec RegEdit with this instruction set
与之前一样,您可以使用 RegisterByFile
获取指令缓冲区并应用更改。这些指令将注册到当前正在执行的 EXE 文件。
仅为当前用户创建菜单
几乎没有应用程序会创建用户级别的上下文菜单,但它们是可能的。这允许一个用户选择退出一个或多个主扩展。问题是,如果根目录也有一个主上下文菜单,用户级别可能会被覆盖。在某些情况下,您的菜单可能会显示两次。不建议这样做,但它可以工作。
Dim ExpMenu As New ContextMenuManager
With ExpMenu
.AddExtensions(...)
...
.Remove = False
End With
ExpMenu.ApplyUserMenuEntries()
如果 .Remove
为 True
,则删除关联,否则添加它们。
属性、方法
Sub AddExtension(ext As String)
Sub AddExtensions(ParamArray ext As String())
将一个或多个扩展添加到您的 Explorer 上下文菜单。在上面的示例中,相同的上下文菜单将与每个列出的扩展相关联。然后,类将添加一个前导点,使其成为合法的扩展(如果缺失)。
所有其他属性都适用于传递的所有扩展。也就是说,为它们全部创建一个上下文菜单和处理程序。但是,您的应用程序可以实现多个菜单——打开、查看、发送。
属性 MenuText As String
要显示在您的扩展上的菜单文本。这应该来自一个 Constant
,以避免意外地向注册表添加不同版本。
属性 ImageFile As String
您希望与菜单文本一起显示的图标的文件名。图像必须是 Icon
,并且必须与您的目标应用程序位于同一个文件夹中。不要包含路径。
属性 ExtendedMenu As Boolean
设置为 True
以便您的菜单项仅在按住 Shift 键并右键单击时显示。
属性 Remove As Boolean
True
或 False
,指示指定的扩展是添加到注册表中还是从注册表中删除。
只读属性 RegEditText As String()
一个 string
数组,包含 RegEdit
.reg 文件的内容,一旦调用了 CreateRegFileText
。
函数 CreateRegFileText(AllUsers As Boolean) As Boolean
使用指定的扩展的指令集填充 RegEditText
。当 Remove
为 true
时生成删除指令,否则生成创建关联的指令。
从技术上讲,您可以为 HKCU 菜单创建 RegEdit
文件内容,但通过 RegEdit
这样做有点傻,因为这会导致 UAC 对话框,而它们可以直接应用。
另请参阅 ApplyUserMenuEntries
和 AppendRegFileText
。
函数 AppendRegFileText() As Boolean
将指令附加到现有的指令集中。这应该在修改关联时使用:首先获取 RegEdit
指令以删除所有扩展;将 Remove
属性重置为 False
;然后 AppendRegFileTextinstructions
附加代码以重新配置新集。
这提供了 Add
和 Remove
操作的指令,全部放在一个文件中。
函数 RegisterExtensions(Optional silent As Boolean = False) As Boolean
使用 RegEditText
数组的当前内容调用 RegEdit
。可选的 Silent
参数会抑制 RegEdit
的 2 个警告对话框,但不会抑制 UAC。
函数 ApplyUserMenuEntries() As Boolean
在 HKCU 中创建或删除您的上下文菜单所需的注册表项,而不是 HKCR。我几乎删除了这个,因为它相当奇怪,但在某些特殊情况下可能有用。
请注意,当您设置的定义存在问题时,所有函数都会返回 False
。例如,MenuText
为空或指定的图像在 EXE 所在的文件夹中找不到。
将命令行发送到第一个应用程序实例
Shell 扩展的少数优点之一是它可以启动目标应用程序的实例,并将所有选定的文件传递给它。通常,使用注册表中的静态上下文菜单条目,会启动多个实例,但VB 可以非常轻松地将命令行“转发”到第一个实例。
- 在您的窗体中添加一个方法来接收新参数。
' when new args are received, add them to the listbox and beep Friend Sub NewArgumentsReceived(args As String()) lbFiles.Items.AddRange(args) Console.Beep() End Sub
- 使您的应用程序成为单实例应用程序。
项目 -> 属性 -> 应用程序选项卡
选中“使应用程序成为单实例应用程序”复选框选项。 - 添加代码以响应 StartupNextInstance 事件。
Visual Basic 的应用程序框架提供了一个事件,当单实例应用程序启动并且应用程序实例已激活时会触发该事件。后来的实例将通过管道将命令行发送到第一个实例。我们可以添加代码来处理这些新参数。
在同一个项目属性选项卡中,单击应用程序事件按钮。这将打开应用程序事件代码模块。在左侧下拉列表中选择 MyApplication Events;在右侧选择
StartupNextInstance
。这将打开/创建事件处理程序。在这里,我们需要将接收到的命令行发送到我们刚刚编写的窗体方法。Private Sub MyApplication_StartupNextInstance(sender As Object, e As ApplicationServices.StartupNextInstanceEventArgs) _ Handles Me.StartupNextInstance ' MyMainForm is the class name of the target form Dim f = Application.MainForm ' use YOUR actual form class name: If f.GetType Is GetType(MyMainForm) Then CType(f, MyMainForm).NewArgumentsReceived(e.CommandLine.ToArray) End If End Sub
几行代码可以响应传递给后续实例的参数。当您的应用程序从
Sub Main
启动并且没有应用程序框架来提供StartupNextInstance
事件时,这会稍微复杂一些。这里解释了为从 Sub Main 启动的应用程序实现它的细节。应用程序以第一个文件启动。其他文件后来通过
StartupNextInstance
传递。选择多个文件时,第一个实例将一个接一个地接收它们,因为Explorer 会尝试启动与所选文件数量相同的实例。
当第一个实例启动时,防火墙可能会寻求允许它“侦听来自其他计算机的连接”的权限。这只是用于接收初始化参数的管道机制。您可能希望告知您的用户,但即使拒绝权限,转发似乎也能正常工作。
在选择与同一上下文菜单关联的 2 个文件时,启动 2 个实例可能会发生异常。如果出现防火墙权限对话框,用户通常无法在连接请求超时之前做出足够快的响应。这对于启动应用程序的Shell 扩展也可能很有用;通常这些会启动一个新实例。
如何使用它
您可以简单地将 ExpContextMenu.vb 文件包含在您的项目中。有一些代码可以在您从 DLL 使用它时尝试解析正确的名称,但未经测试。
演示
演示是一个简单的应用程序,用于注册或注销一组三个文件扩展名。代码通常会更有趣。如其他地方所述,由于它侦听由后续实例传递的命令行,因此运行它可能会导致防火墙操作,具体取决于您的配置。
要测试或运行它,您必须在您的测试文件夹中有一个编译后的副本。这是必需的:ExplorerMenuManager
不会为不存在的文件创建菜单条目。它也需要用于测试命令行转发。
- 在某处创建一个测试文件夹,例如 C:\Temp。
- 将 TestFiles 文件夹中的文件复制到该测试文件夹。这些是虚假/空的文本文件,具有演示中使用的扩展名和图标。
- 将 VS 配置为在该文件夹中启动。这是为了模拟部署的运行时条件。
- 打开项目属性。
- 在调试选项卡的启动选项下,将工作目录设置为您创建的测试文件夹。
- 编译演示程序并将 EXE 复制到同一个测试文件夹。
注意:演示程序不会抑制任何 RegEdit
确认对话框。通过使用可选的 silent 参数可以调用 RegEdit 的静默模式。
ExpMenu.RegisterRegFile(RegFile, True)
从 VS 或编译后的文件运行演示程序。注册一个或两个文件扩展名。然后从Explorer,右键单击,您应该会在菜单上看到新条目。
注销它们,菜单将不再出现。
图标文件必须位于目标应用程序的同一文件夹中,并且图标文件和 EXE 都必须存在。演示程序包含一个 Test Files 文件夹,其中仅包含演示中使用的扩展名的空文本文件。
如果您在测试文件夹中有应用程序的编译副本,您还可以演示命令行转发。
- 从 VS 项目或磁盘文件运行第一个应用程序实例(如果您已将启动选项的工作目录设置为您的测试文件夹,则 VS 实例将计为第一个实例)。
- 如果您还没有这样做,请为您的应用程序注册 zig 或 zag 文件。
- 从 explorer 中,打开您的上下文菜单并单击已注册到应用程序的文件(演示中的 .zig、.zag、.zoe)。这应该会在磁盘上启动 EXE,它会将命令行参数转发给第一个实例。
摘要
标准或静态上下文菜单比Shell 扩展更轻量级,更容易实现,主要限制是它们不会自动将多个文件发送到目标应用程序的单个实例。
ExplorerMenuManager
提供了轻松添加和删除上下文菜单处理程序并避免 Shell 扩展的手段。它的主要目的是提供灵活性,允许单个用户根据自己的需求定制您的上下文菜单。因此,ExplorerMenuManager
主要面向与全局关联配合使用,但也可以按用户模型工作。
演示还显示了如何通过处理 MyApplication.StartupNextInstance
事件来接收单实例应用程序中的新命令行参数,从而克服多实例限制。
历史
- 2015.09.09 - 初始版本