你做想要一个ShellExtension
你想要的并不像你想象的那么简单。选择多个文件的正常行为是在新的 Window/App 实例中打开每个文件。实际上,它只是将选定的文件发送到已注册的应用程序,然后由应用程序决定如何使用它们。
不过,至少有 1 种快速简便的替代方法:
方法一:使用发送至
打开 Send To 文件夹 ("C:\Users\YOURNAME\AppData\Roaming\Microsoft\Windows\SendTo") 并为应用添加一个条目。目标是您希望将文件选择提供/发送到的应用程序:
"C:\Program Files\That Other App\OtherApp.exe "
您不需要“%1”占位符或其他任何东西。您不必编写中介来执行任何操作,只需将文件直接发送到实际应用程序即可。只要应用程序在命令行上接受多个文件,它就可以正常工作。
唯一的小问题是它驻留在“共享”或通用子菜单上,而不是顶级上下文菜单上。它也不是“智能”的,因为它可用于任何文件扩展名,不像适当的 ContextMenu 处理程序,但它是一种快速、简单、无代码的解决方案,已经存在了很长时间。
方法二:改变动词限定词
您还可以更改动词限定词/模式,这听起来是最简单的方法。以 VideoLan 的 VLC 播放器为例:
如果您单击多个 .MP4 文件而不是打开多个实例,则会打开其中一个,其余的将排队等待播放。这是通过修改注册表中的动词来完成的:
+ VLC.MP4
+ shell
+ Open
- MultiSelectModel = Player
+ Command
- (Default) "C:\Program Files.... %1"
MultiSelectModel 是Open 的修饰语动词:
-
Single 表示仅支持单个项目的动词
-
播放器用于支持任意数量项目的动词
-
文档用于为每个项目创建顶级窗口的动词
对于我的 MediaProps 小程序,由于它涉及相同的文件类型,我通过添加设置为 MultiSelectModel.Player 的 ViewProps 动词来将动词搭载到 VLC 的文件类型上,并且通常在我的动词不会混淆 VLC。
很遗憾,我还没有发现有什么不对劲的地方。 Windows 似乎仍然没有按预期将所有文件粘合在一起 - 即使我自己制作动词。在注册表配置或应用程序中缺少一个步骤 - 但有两种其他方法可以做同样的事情,我从未进一步调查。
方法3:创建ShellExtension / ContextMenu Handler
许多提议的解决方案最终成为Whack-a-Mole 的游戏,您必须在干预应用程序中修复相同的 1 file-1 实例问题,以便它可以将连接的参数提供给最终参与者。由于最终结果是有一个 Explorer ContextMenu 来做一些有用的事情,所以我们只需为这个其他应用程序构建一个 ShellExtension 。
这很简单,因为框架已经完成并在 CodeProject 上可用:How to Write Windows Shell Extension with .NET Languages。这是一篇 MS-PL 文章,包含已完成的 ShellExtension 项目。
经过一些修改,这将完美地适用于:
- 为多种文件类型设置关联
- 点击收集多个文件
- 将它们格式化为命令行参数集
- 将命令行传递给实际的工作应用程序
- 提供自定义 ContentMenu
- 显示一个时髦的菜单图标
此测试平台是一个小程序,用于显示媒体文件的 MediaInfo 属性(如持续时间、帧大小、编解码器、格式等)。除了接受 Dropped 文件之外,它还使用 ContextMenu DLL 帮助程序来接受在 Explorer 中选择的多个文件并将它们提供给 Single Instance display 应用程序。
非常重要的注意事项
自从这篇文章第一次发布以来,我已经修改和更新了原始的 MS-PL 文章,使其更易于使用。修订版也在 CodeProject Explorer Shell Extensions in .NET (Revised) 上,仍然包含 VB 和 C# 版本。
在修订版中,不必到处进行更改,而是将它们合并为一个变量块。该文章还解释了您可能想要使用 C# 版本的原因,并提供了文章链接,这些文章解释了为什么将托管代码用于 Shell 扩展不是一个好主意。
“模型”仍然是外壳扩展的模型,用于简单地启动相关应用。
这个答案的平衡对于一般概念和背景仍然值得一读。即使 代码更改 部分的大部分内容不适用于修订版,事后对其进行更改似乎也不合适。
1.更新程序集/项目值
例如,我将程序集名称更改为“MediaPropsShell”。我还删除了根命名空间,但这是可选的。
添加您选择的 PNG 图标。
选择合适的平台。由于原始版本有 2 个安装程序,您可能必须专门为 32 位操作系统构建 x86 版本。 AnyCPU 适用于 64 位操作系统,我不确定 x86。大多数使用此模型的系统都为 shell 扩展帮助程序提供 32 位和 64 位 DLL,但过去大多数系统都不能基于 NET,而 AnyCPU 是一个选项。
将目标平台保持为 NET 4。 如果您没有阅读 CodeProject 文章或之前没有研究过这个,这很重要。
2。代码更改
正如在 CodeProject 上发布的那样,处理程序也只传递一个文件,并且只将自己与一种文件类型相关联。下面的代码实现了多种文件类型的处理程序。您还需要修复菜单名称等。所有更改都在下面的代码中注明,前言为{PL}:
' {PL} - change the GUID to one you create!
<ClassInterface(ClassInterfaceType.None),
Guid("1E25BCD5-F299-496A-911D-51FB901F7F40"), ComVisible(True)>
Public Class MediaPropsContextMenuExt ' {PL} - change the name
Implements IShellExtInit, IContextMenu
' {PL} The nameS of the selected file
Private selectedFiles As List(Of String)
' {PL} The names and text used in the menu
Private menuText As String = "&View MediaProps"
Private menuBmp As IntPtr = IntPtr.Zero
Private verb As String = "viewprops"
Private verbCanonicalName As String = "ViewMediaProps"
Private verbHelpText As String = "View Media Properties"
Private IDM_DISPLAY As UInteger = 0
Public Sub New()
' {PL} - no NREs, please
selectedFiles = New List(Of String)
' Load the bitmap for the menu item.
Dim bmp As Bitmap = My.Resources.View ' {PL} update menu image
' {PL} - not needed if you use a PNG with transparency (recommended):
'bmp.MakeTransparent(bmp.GetPixel(0, 0))
Me.menuBmp = bmp.GetHbitmap()
End Sub
Protected Overrides Sub Finalize()
If (menuBmp <> IntPtr.Zero) Then
NativeMethods.DeleteObject(menuBmp)
menuBmp = IntPtr.Zero
End If
End Sub
' {PL} dont change the name (see note)
Private Sub OnVerbDisplayFileName(ByVal hWnd As IntPtr)
'' {PL} the command line, args and a literal for formatting
'Dim cmd As String = "C:\Projects .NET\Media Props\MediaProps.exe"
'Dim args As String = ""
'Dim quote As String = """"
'' {PL} concat args
For n As Integer = 0 To selectedFiles.Count - 1
args &= String.Format(" {0}{1}{0} ", quote, selectedFiles(n))
Next
' Debug command line visualizer
MessageBox.Show("Cmd to execute: " & Environment.NewLine & "[" & cmd & "]", "ShellExtContextMenuHandler")
'' {PL} start the app with the cmd line we made
'If selectedFiles.Count > 0 Then
' Process.Start(cmd, args)
'End If
End Sub
#Region "Shell Extension Registration"
' {PL} list of media files to show this menu on (short version)
Private Shared exts As String() = {".avi", ".wmv", ".mp4", ".mpg", ".mp3"}
<ComRegisterFunction()>
Public Shared Sub Register(ByVal t As Type)
' {PL} use a loop to create the associations
For Each s As String In exts
Try
ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, s,
"MediaPropsShell.MediaPropsContextMenuExt Class")
Catch ex As Exception
Console.WriteLine(ex.Message)
Throw ' Re-throw the exception
End Try
Next
End Sub
<ComUnregisterFunction()>
Public Shared Sub Unregister(ByVal t As Type)
' {PL} use a loop to UNassociate
For Each s As String In exts
Try
ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, s)
Catch ex As Exception
Console.WriteLine(ex.Message) ' Log the error
Throw ' Re-throw the exception
End Try
Next
End Sub
#End Region
在IShellExtInit Members REGION 中也需要稍作更改:
Public Sub Initialize(pidlFolder As IntPtr, pDataObj As IntPtr,
hKeyProgID As IntPtr) Implements IShellExtInit.Initialize
If (pDataObj = IntPtr.Zero) Then
Throw New ArgumentException
End If
Dim fe As New FORMATETC
With fe
.cfFormat = CLIPFORMAT.CF_HDROP
.ptd = IntPtr.Zero
.dwAspect = DVASPECT.DVASPECT_CONTENT
.lindex = -1
.tymed = TYMED.TYMED_HGLOBAL
End With
Dim stm As New STGMEDIUM
' The pDataObj pointer contains the objects being acted upon. In this
' example, we get an HDROP handle for enumerating the selected files
' and folders.
Dim dataObject As System.Runtime.InteropServices.ComTypes.IDataObject = Marshal.GetObjectForIUnknown(pDataObj)
dataObject.GetData(fe, stm)
Try
' Get an HDROP handle.
Dim hDrop As IntPtr = stm.unionmember
If (hDrop = IntPtr.Zero) Then
Throw New ArgumentException
End If
' Determine how many files are involved in this operation.
Dim nFiles As UInteger = NativeMethods.DragQueryFile(hDrop,
UInt32.MaxValue, Nothing, 0)
' ********************
' {PL} - change how files are collected
Dim fileName As New StringBuilder(260)
If (nFiles > 0) Then
For n As Long = 0 To nFiles - 1
If (0 = NativeMethods.DragQueryFile(hDrop, CUInt(n), fileName,
fileName.Capacity)) Then
Marshal.ThrowExceptionForHR(WinError.E_FAIL)
End If
selectedFiles.Add(fileName.ToString)
Next
Else
Marshal.ThrowExceptionForHR(WinError.E_FAIL)
End If
' {/PL}
' *** no more changes beyond this point ***
' [-or-]
' Enumerates the selected files and folders.
'...
Finally
NativeMethods.ReleaseStgMedium((stm))
End Try
End Sub
原始代码实际上确实包含注释掉的多文件方法的代码。在添加之前我实际上没有看到它。变化的部分在星弦之间。
另外,遗憾的是,使用Option Strict,您将不得不对 Microsoft 的代码进行 10 次左右的小改动。只需接受 IntelliSense 建议的更改即可。
重要提示
代表 EXE“引擎”提供 ContextMenu 服务的单独 DLL 模型非常常见。这就是您经常在文件夹中看到的所有xxxShell.DLL 文件以及程序可执行文件。此处的区别在于您正在构建 DLL,而不是相关应用的作者。
- 除一项之外的所有更改都在
FileContextMenuExt 类中
- 请务必更改 GUID,否则您的处理程序可能会与基于相同 MS 模板的其他处理程序发生冲突!
Tools 菜单上有一个方便的实用程序。
- BMP/PNG 是可选的
- 最初的 MS 版本仅显示所选文件的名称。所以相关程序命名为
OnVerbDisplayFileName。如你所见,我没有改变这一点。如果您更改它以匹配您的实际操作,您还需要在IContextMenu 的 PInvoke 重代码中更改对它的一些引用。不过,除了你之外,没有人会看到这个名字。
- 调试消息框是调用操作的全部。您可以查看我使用的实际代码。
原始MS项目中的自述文件描述了这一点,但是在编译后,将文件复制到它将驻留的位置并注册它:
regasm <asmfilename>.DLL /codebase
注销:
regasm <asmfilename>.DLL /unregister
使用您的Microsoft.NET\Framework64\v4.0.xxxx 文件夹中的RegAsm。这必须从具有管理员权限的命令窗口(或等效脚本)中完成。或者,对于已部署的应用,您可以让目标应用使用 Public Regster/UnRegister 方法注册/注销帮助程序 DLL。
警告:仔细更改代码并测试循环和字符串格式等内容在编译之前;您希望尽可能少的编译测试迭代。原因是一旦您激活了新的上下文菜单,该 DLL 就会被 Explorer 使用,并且不能被新版本替换。您必须终止 explorer.exe 进程(不仅仅是文件资源管理器!)才能注册并尝试新的构建。
可能还有另一种方法,但我只是关闭任何资源管理器窗口,然后注销并重新打开。
测试
如果我右键单击其中一种已注册的文件类型,我会按预期获得带有正确菜单文本和位图图像的菜单:
click for larger image
如果我单击,小程序会按预期出现 在一个实例中包含多个文件:
click for larger image
请注意底部的 Prev/Next 按钮是如何启用从一个文件移动到另一个文件的,而仅加载 1 个文件时不是这种情况。
在我的机器上工作TM
资源
How to Write Windows Shell Extension with .NET Languages。这是一篇 MS-PL 文章,包含完成的 ShellExtension 项目。以上是一组模组,使其可以与多个扩展名和多个文件一起使用,因此需要以原始项目为起点。
Best Practices for Shortcut Menu Handlers and Multiple Verbs
Choosing a Static or Dynamic Shortcut Menu Method
Verbs and File Associations