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

时间:2014-11-23 11:38:24

标签: c# .net vb.net windows explorer

  

情况

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

MyProgram.exe "file1" "file2"

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

为了优化我的时间,我想通过右键单击 Windows资源管理器中的某些文件来加载多个文件(例如: 选择5个文件>做正确 - 点击>选择"在MyProgram中打开"命令

我知道如何创建所需的注册表项以在特定文件类型的上下文菜单中添加该命令,这不是问题。

  

问题

此第三方程序没有任何驱动程序,shell扩展或方法可以从上下文菜单中捕获多个文件,因此而不是如果我从资源管理器中选择2个文件,每个文件都在程序的单独实例中打开,我不知道开发司机,所以司机不是我想要的。

  

焦点

我愿意接受建议,也许这不是有效的方法,但似乎是最简单的方法:

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

换句话说,在第三方应用程序中选择多个文件一次打开所有文件时,只需一个简单的加载器即可从上下文菜单中使用它。

  

问题

首先,我想知道是否存在一个已知术语来命名应用程序的这个东西,它能够在同一个实例中加载多个文件,从explorer中选择文件然后选择contextmenu。我想研究那个词。

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

如何开始开发?

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

3 个答案:

答案 0 :(得分:22)

想要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动词将我的动词搭载到VLC的文件类型上,该动词设置为MultiSelectModel.Player并且通常在到目前为止,我的动词并没有混淆VLC。

不幸的是,我还有一些问题尚未确定。 Windows似乎仍然没有像预期的那样粘合所有文件 - 即使我自己制作动词。在注册表配置或应用程序中缺少一个步骤 - 但是还有其他两种方法可以做同样的事情,我从未进一步调查过。


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

许多提议的解决方案最终成为Whack-a-Mole的游戏,您必须在干预的应用程序中修复相同的1个文件1实例问题,以便它可以将连接的参数提供给最终的参与者。由于最终结果是让资源管理器ContextMenu 做一些有用的事情,我们只需为这个其他应用程序构建一个 ShellExtension

这是 easy ,因为框架已在CodeProject上完成并可用:How to Write Windows Shell Extension with .NET Languages。这是一篇MS-PL文章,附有完成的ShellExtension项目。

通过一些修改,这将完美地适用于:

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

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


非常重要的注意事项

自首次发布以来,我已经修订并更新原始MS-PL文章,使其更易于使用。修订版也在CodeProject Explorer Shell Extensions in .NET (Revised),并且仍包含VB和C#版本。

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

'模型'仍然是Shell扩展程序,只需启动相关的应用程序。

对于一般概念和背景,这个答案的平衡仍然值得一读。即使很多代码更改部分不适用于修订版,但事后似乎也没有好好改变它。


<强> 1。更新装配/项目值

例如,我将程序集名称更改为&#34; MediaPropsShell&#34;。我还删除了根命名空间,但这是可选的。

添加您选择的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区域内也需要更改一下:

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,你将不得不作出10种左右的微小变化,以微软&#39; S码。只需接受IntelliSense建议的更改即可。


重要提示

代表EXE&#34;引擎&#34;提供ContextMenu服务的单独DLL的模型。 非常常见。这就是您经常在文件夹中看到的所有xxxShell.DLL文件以及程序可执行文件。这里的区别在于正在构建DLL而不是相关应用程序的作者。

  1. 除{1}之外的所有更改都在FileContextMenuExt
  2. 请务必更改GUID,否则您的处理程序可能会根据相同的MS模板与其他人发生冲突!在Tools菜单上有一个方便的实用程序。
  3. BMP / PNG是可选的
  4. 原始MS版本只显示所选文件的名称。因此,相关程序名为OnVerbDisplayFileName。如你所见,我没有改变它。如果您更改它以匹配您的实际操作,您还需要在IContextMenu的PInvoke重码中更改对它的一些引用。没有人,但你会看到这个名字。
  5. 调试MessageBox就是调用操作的全部内容。您可以看到我使用的实际代码。
  6. 原始MS项目中的自述文件描述了这一点,但在编译之后,将文件复制到它将驻留的位置并注册它:

    regasm <asmfilename>.DLL /codebase
    

    取消注册:

    regasm <asmfilename>.DLL /unregister
    

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


    警告:让您的代码更改仔细,并在编译之前测试循环和字符串格式等内容;您希望尽可能少的编译测试迭代。原因是,一旦激活新的上下文菜单,DLL就会被资源管理器使用,并且不能被新的构建替换。您必须终止explorer.exe进程(不仅仅是文件资源管理器!)才能注册并尝试新的构建。

    可能还有另一种方式,但我只关闭任何资源管理器Windows,然后注销并重新开启。


    测试

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

    enter image description here

    click for larger image

    如果我点击,applet会按预期出现在一个实例中有多个文件:

    enter image description here enter image description here

    click for larger image

    请注意底部的Prev / Next按钮如何启用从一个文件移动到另一个文件,而仅在加载1个文件时不是这种情况。

    适用于我的机器 TM


    资源

    How to Write Windows Shell Extension with .NET Languages。这是一篇MS-PL文章,其中包含一个完成的ShellExtension项目。以上是一组mod,使其可以使用多个扩展和多个文件,因此需要原始项目作为起点。

    Best Practices for Shortcut Menu Handlers and Multiple Verbs

    Choosing a Static or Dynamic Shortcut Menu Method

    Verbs and File Associations

答案 1 :(得分:3)

为什么不编写带有单个实例应用程序的.exe文件。

然后在该新应用程序中捕获 MyApplication 类中提供的 MyApplication_StartupNextInstance 以捕获资源管理器推送的所有文件,可能让应用程序等待一秒或2确保资源管理器不发送以下文件,然后将这些文件合并为1个字符串并将其解析为第3方应用程序。

如果有兴趣我可以提供一些代码让你入门

答案 2 :(得分:2)

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


所以,这就是它在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