实施Explorer ContextMenu并将多个文件传递给一个程序实例

情况

我有一个通过CLI接受多个文件的第三方GUI应用程序,例如:

MyProgram.exe "file1" "file2" 

然后,所有文件一次加载到应用程序的同一个实例中。

为了优化我的时间,我想通过右键单击Windows资源pipe理器中的某些文件来加载多个文件(例如: select5个文件>右键单击>select“在MyProgram中打开”命令

我知道如何创build所需的registry项,以在特定文件types的上下文菜单中添加该命令,这不是问题。

问题

这第三方程序没有任何驱动程序,shell扩展或方法,可以从上下文菜单中捕获多个文件,所以而不是如果我从资源pipe理器中select2个文件,每个文件打开一个单独的程序实例,我没有开发驱动程序的想法,所以一个驱动程序不是我要找的。

焦点

我愿意提出build议,也许这不是有效的方法,但似乎是最简单的方法:

我的想法是开发一个迷你的CLI应用程序来捕获这些多个文件(可能基于Windows消息或在没有活动,我不知道这就是为什么我问),将这些文件/参数写入一个文本文件,然后join所有参数在一行中调用我的第三方程序与这些参数一次加载在这个程序的单个实例的所有文件。

换句话说,只需一个简单的加载程序就可以在select多个文件的同时从上下文菜单中使用它来在第三方应用程序中同时打开所有文件。

首先,我想知道是否存在一个已知的术语来命名这个应用程序的东西,它能够在同一个实例中加载多个文件,从资源pipe理器中select文件,然后select上下文菜单。 我想研究这个术语。

在VB.NET / C#控制台应用程序中,哪个可能是最有效的方法来完成这个任务? (不是司机)

如何开始发展呢?

来自已知页面的任何现有的源代码示例,如codeproject …?

你想要一个ShellExtension

你想要的不像你想的那么简单。 多个文件选择的正常行为是在新的Window / App实例中打开每个文件。 实际上,它只是将选定的文件发送到注册的应用程序,并将其留给应用程序来决定如何使用它们。

尽管至少有一个快速而简单的选择:

方法1:使用发送到

打开“ Send To文件夹( "C:\Users\YOURNAME\AppData\Roaming\Microsoft\Windows\SendTo" )并为应用程序添加一个条目。 目标将是你想要喂养/发送文件选择的应用程序:

 "C:\Program Files\That Other App\OtherApp.exe " 

您不需要“%1”占位符或其他任何东西。 你不必写一个中介来做任何事情,只需要把文件直接发送到实际的应用程序。 它会正常工作,只要应用程序将接受命令行上的多个文件。

唯一的小事是它驻留在“共享”或一般的子菜单而不是顶级的上下文菜单。 与适当的ContextMenu处理程序不同的是,它可用于任何文件扩展名也不“聪明”,但它是一个快速而简单的无代码解决方案,已经存在了很长时间。


方法2:更改动词限定词

您也可以更改动词限定符/模式,这听起来像最简单的方法。 举个例子,VideoLan的VLC播放器:

如果您单击多个.MP4文件而不是打开多个实例,则会打开其中一个文件,其余文件将排队等待播放。 这是通过修改注册表中的动词完成的:

 + VLC.MP4 + shell + Open - MultiSelectModel = Player + Command - (Default) "C:\Program Files.... %1" 

MultiSelectModelOpen 动词的修饰符:

  • 单个用于仅支持单个项目的动词
  • 支持任意数量项目的动词的玩家
  • 为每个项目创建顶级窗口的动词文档

对于我的MediaProps小应用程序,由于它涉及到相同的文件类型,我通过添加一个ViewProps动词,将其设置为MultiSelectModel.Player ,将我的动词搭载到VLC的文件类型上,并且通常在我的动词不会混淆VLC 。

不幸的是,还有一些我还没有确定的错误。 Windows似乎还没有像预期的那样把所有的文件粘在一起 – 即使我自己动词。 在注册表配置或应用程序中有一个步骤缺失 – 但有两种方法来做同样的事情,我从来没有进一步调查。


方法3:创建ShellExtension / ContextMenu处理程序

许多提出的解决方案最终都是一个Whack-a-Mole游戏,你必须在一个干预的应用程序中修复相同的1个文件-1实例问题,这样就可以将连接的参数提供给最终的actor。 由于最终的结果是有一个Explorer的ContextMenu做一些有用的事情,让我们只这个其他应用程序建立一个ShellExtension

这很容易,因为一个框架已经完成CodeProject上可用: 如何编写Windows外壳扩展与.NET语言 。 这是一个完整的ShellExtension项目的MS-PL文章。

通过一些修改,这将完美地工作:

  • 多种文件类型设置关联
  • 收集多个文件点击
  • 将它们格式化为命令行参数集
  • 将命令行传递给实际的工作应用程序
  • 提供一个自定义的ContentMenu
  • 显示一个时髦的菜单图标

这个测试平台是一个小程序,用于显示媒体文件的MediaInfo属性(如持续时间,帧大小,编解码器,格式等)。 除了接受Dropped文件之外,它还使用ContextMenu DLL助手来接受在资源管理器中选择的多个文件,并将它们提供给单一实例显示应用程序。


非常重要的注意

由于这是第一次发布,我已经修改和更新了原来的MS-PL文章使它更容易使用。 该修订也是在.NET(修订)的 CodeProject 资源管理器外壳扩展 ,仍然包含一个VB和C#版本。

在修改后的版本中,不必在各处进行更改,而是将它们合并为一个变量块。 本文还解释了为什么您可能想要使用C#版本,并提供了文章的链接,解释为什么使用Shell Extensions的托管代码不是一个好主意

“模型”仍然是一个壳牌扩展的简单启动相关的应用程序。

这个答案的平衡仍然值得一读的一般概念和背景。 尽管许多“ 代码更改”部分不适用于修订版本,但事实并非如此。


1.更新组件/项目值

例如,我将程序集名称更改为“MediaPropsShell”。 我也删除了根名称空间,但是这是可选的。

添加您选择的PNG图标。

选择适当的平台。 由于原来有2个安装程序,您可能必须专门为32位操作系统构建x86版本。 任何CPU工作正常的64位操作系统,我不知道x86。 大多数使用这个模型的系统为外壳扩展助手提供了一个32位和64位的DLL,但是大多数以前的系统都不能以AnyCPU为选项的.NET。

保持目标平台为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区域中,稍微需要更改一下:

 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而不是有问题的应用程序的作者。

  1. 除了一个之外的所有更改都在FileContextMenuExt类中
  2. 请务必更改GUID,否则您的处理程序可能会与基于相同MS模板的其他人发生冲突! 在Tools菜单上有一个方便的Tools
  3. BMP / PNG是可选的
  4. 最初的MS版本只显示了所选文件的名称。 所以相关过程被命名为OnVerbDisplayFileName 。 如你所见,我没有改变这一点。 如果您将其更改为与您的实际操作相匹配,则还需要在PInvoke用于IContextMenu大量代码中更改对其的引用。 除了你以外,没有人会看到这个名字。
  5. 一个调试MessageBox就是调用操作的所有东西。 你可以看到我使用的实际代码。

最初的MS项目中的自述文件描述了这一点,但在编译之后,将文件复制到它将驻留的位置并注册它:

 regasm <asmfilename>.DLL /codebase 

取消注册:

 regasm <asmfilename>.DLL /unregister 

使用Microsoft.NET\Framework64\v4.0.xxxx文件夹中的RegAsm 。 这将必须从具有管理员权限的命令窗口(或等效的脚本)完成。 或者,对于已部署的应用程序,您可以使用Public Regster/UnRegister方法向目标应用程序注册/取消注册助手DLL。


警告: 在编译之前,请 仔细更改代码并测试诸如循环和字符串格式之类的东西; 您希望尽可能少的编译测试迭代。 原因是,一旦激活了新的上下文菜单,DLL就被Explorer使用,不能被新的版本所取代。 您必须终止explorer.exe进程(不只是文件资源管理器!)注册并尝试新的版本。

有可能是另一种方式,但我只是关闭任何资源管理器窗口,然后注销,然后再回来。


测试

如果我右键单击其中一个已注册的文件类型,我会得到正确的菜单文本和位图图像的菜单:

在这里输入图像说明

点击放大图像

如果我点击,小程序按照预期在一个实例中出现多个文件:

在这里输入图像说明 在这里输入图像说明

点击放大图像

请注意底部的“上一页”/“下一页”按钮是如何从文件移动到文件的,而不是仅在加载1个文件的情况下。

适用于My Machine TM


资源

如何用.NET语言编写Windows Shell扩展 。 这是一个完整的ShellExtension项目的MS-PL文章。 以上是一组模块,使其能够处理多个扩展名和多个文件,因此需要原始项目作为起点。

快捷菜单处理程序和多个动词的最佳做法

选择静态或动态快捷菜单方法

动词和文件关联

为什么不写一个.exe与单一实例应用程序检查。

然后在那个新的应用程序中捕获MyApplication类中提供的MyApplication_StartupNextInstance,以捕获由explorer推送的所有文件,也许让应用程序等待一秒钟或2秒钟,以确保没有以下文件被explorer发送,然后将这些文件合并为1字符串并解析它们到您的第三方应用程序。

如果感兴趣,我可以把一些代码让你开始

编辑 :我丢弃了这个解决方案,因为我发现这种方法有很坏的缺点。


所以,这是它在VB.Net中看起来如此简单的方法(感谢@ Roy van der Velde

它以这种格式将文件路径存储在字符串生成器中:

 "File1" "File2 "File3" 

在不活动时间(使用Timer )之后,文件路径参数被传递给指定的应用程序,就这些了。

代码是可重复的和可定制的:)

它应该被标记为单实例如果VB.Net,如果C#然后使用互斥或​​…我不知道如何。

主要表格类别:

 Public Class Main Public Sub New() ' This call is required by the designer. InitializeComponent() ' Add any initialization after the InitializeComponent() call. Me.Size = New Size(0, 0) Me.Hide() Me.SuspendLayout() End Sub End Class 

应用程序事件类:

 #Region " Option Statements " Option Strict On Option Explicit On Option Infer Off #End Region #Region " Imports " Imports Microsoft.VisualBasic.ApplicationServices Imports System.IO Imports System.Text #End Region Namespace My ''' <summary> ''' Class MyApplication. ''' </summary> Partial Friend Class MyApplication #Region " Properties " ''' <summary> ''' Gets the application path to pass the filepaths as a single-line argument. ''' </summary> ''' <value>The application path.</value> Private ReadOnly Property AppPath As String Get Return Path.Combine(My.Application.Info.DirectoryPath, "MP3GainGUI.exe") End Get End Property ''' <summary> ''' Gets the inactivity timeout, in milliseconds. ''' </summary> ''' <value>The inactivity timeout, in milliseconds.</value> Private ReadOnly Property TimeOut As Integer Get Return 750 End Get End Property ''' <summary> ''' Gets the catched filepaths. ''' </summary> ''' <value>The catched filepaths.</value> Private ReadOnly Property FilePaths As String Get Return Me.filePathsSB.ToString End Get End Property #End Region #Region " Misc. Objects " ''' <summary> ''' Stores the catched filepaths. ''' </summary> Private filePathsSB As StringBuilder ''' <summary> ''' Keeps track of the current filepath count. ''' </summary> Private filePathCount As Integer ''' <summary> ''' Timer that determines whether the app is inactive. ''' </summary> Private WithEvents inactivityTimer As New Timer With { .Enabled = False, .Interval = Me.TimeOut } #End Region #Region " Event Handlers " ''' <summary> ''' Handles the Startup event of the application. ''' </summary> ''' <param name="sender">The source of the event.</param> ''' <param name="e">The <see cref="ApplicationServices.StartupEventArgs"/> instance containing the event data.</param> Private Sub Me_Startup(ByVal sender As Object, ByVal e As StartupEventArgs) _ Handles Me.Startup Select Case e.CommandLine.Count Case 0 ' Terminate the application. e.Cancel = True Case Else ' Add the filepath argument and keep listen to next possible arguments. Me.filePathsSB = New StringBuilder Me.filePathsSB.AppendFormat("""{0}"" ", e.CommandLine.Item(0)) Me.filePathCount += 1 With Me.inactivityTimer .Tag = Me.filePathCount .Enabled = True .Start() End With End Select End Sub ''' <summary> ''' Handles the StartupNextInstance event of the application. ''' </summary> ''' <param name="sender">The source of the event.</param> ''' <param name="e">The <see cref="ApplicationServices.StartupNextInstanceEventArgs"/> instance containing the event data.</param> Private Sub Me_StartupNextInstance(ByVal sender As Object, ByVal e As StartupNextInstanceEventArgs) _ Handles Me.StartupNextInstance Select Case e.CommandLine.Count Case 0 ' Terminate the timer and run the application. Me.TerminateTimer() Case Else ' Add the filepath argument and keep listen to next possible arguments. Me.filePathsSB.AppendFormat("""{0}"" ", e.CommandLine.Item(0)) Me.filePathCount += 1 End Select End Sub ''' <summary> ''' Handles the Tick event of the InactivityTimer control. ''' </summary> ''' <param name="sender">The source of the event.</param> ''' <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param> Private Sub InactivityTimer_Tick(ByVal sender As Object, ByVal e As EventArgs) _ Handles inactivityTimer.Tick Dim tmr As Timer = DirectCast(sender, Timer) If DirectCast(tmr.Tag, Integer) = Me.filePathCount Then Me.TerminateTimer() Else tmr.Tag = Me.filePathCount End If End Sub #End Region #Region " Methods " ''' <summary> ''' Terminates the inactivity timer and runs the application. ''' </summary> Private Sub TerminateTimer() Me.inactivityTimer.Enabled = False Me.inactivityTimer.Stop() Me.RunApplication() End Sub ''' <summary> ''' Runs the default application passing all the filepaths as a single-line argument. ''' </summary> Private Sub RunApplication() #If DEBUG Then Debug.WriteLine(Me.FilePaths) #End If Try Process.Start(Me.AppPath, Me.FilePaths) Catch ex As FileNotFoundException ' Do Something? End Try ' Terminate the application. MyBase.MainForm.Close() End Sub #End Region End Class End Namespace