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

Posted

技术标签:

【中文标题】实现 Explorer ContextMenu 并将多个文件传递给一个程序实例【英文标题】:Implement Explorer ContextMenu and pass multiple files to one program instance 【发布时间】:2015-01-21 04:50:09 【问题描述】:

情况

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

MyProgram.exe "file1" "file2"

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

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

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

问题

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

专注

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

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

换句话说,只需一个简单的加载程序,即在第三方应用程序中选择多个文件时要将其从ContextMenu使用它。

问题

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

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

如何开始开发这个?

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

【问题讨论】:

您要查找的搜索词是“.net 4 explorer context menu”。 @Andrew Morton 好吧,这不是一个术语,但感谢您的帮助,无论如何按该搜索模式搜索我只找到有关如何将项目/命令添加到上下文菜单中的信息......没什么有趣的,我已经学会了这些东西。 我的谷歌必须训练有素,因为找到的第一个项目是.NET Shell Extensions - Shell Context Menus。 是的,正如我所说,这没什么有趣的,或者至少我不知道这是否是我真正需要完成的事情(如果是这样的话,对不起我的无知),因为在那示例似乎只讨论如何将项目添加/注册到上下文菜单中。谢谢 了解解决方案应该使用的应用程序/实用程序可能会有所帮助。 【参考方案1】:

想要一个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"
             

MultiSelectModelOpen 的修饰语动词

Single 表示仅支持单个项目的动词 播放器用于支持任意数量项目的动词 文档用于为每个项目创建***窗口的动词

对于我的 MediaProps 小程序,由于它涉及相同的文件类型,我通过添加设置为 MultiSelectModel.PlayerViewProps 动词来将动词搭载到 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(" 010 ", 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

【讨论】:

哇!多么好的答案,谢谢!,“sendto”方法给了我一个想法,“sendto”功能可以从应用程序内部调用,可能是 P/Invoking 或其他东西?您如何看待这种方法?它可以像 sendto 功能一样工作吗?: · 创建一个上下文菜单来调用一个 CLI 应用程序,该应用程序将一个一个地捕获所有选定的文件,以便对每个文件或所有文件一次使用“sendto”功能,将其发送到我的程序。您认为它可以完成并且可以按预期工作吗? 只知道:您是否使用“B”方法测试了透明的PNG图像?,当我没有读完您的答案时,要测试的东西太多了! :) Send-To via PInvoke 听起来像是一个杂物,最终可能会导致更多的 Whack-a-Mole,尤其是当属性 ContextMenu 助手如此简单时。查看大图 - 尽管透明区域非常小,但它使用的是透明 PNG。 我为我一直从事的项目实施了 SendTo 选项,但在发布后开始收到大量关于用户收到“文件名或扩展名太长”的投诉。在弹出窗口中向他们报告异常。果然,SendTo 方法受到 shell 的 32k 环境限制(包括要通过链接执行的命令)的限制。在不实施完整的 ShellExtension 解决方案的情况下,有什么想法或想法可以解决这个问题? 有趣。我会尝试 Explorer-Context-Menu 您还可以配置应用程序,以便当他们在应用程序已经打开时使用菜单时,已经运行的应用程序响应【参考方案2】:

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


所以,这就是它在 VB.Net 中的外观这种简单的方法(感谢@Roy van der Velde

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

"File1" "File2 "File3"

在不活动时间之后(使用定时器),文件路径参数被传递给指定的应用程序,仅此而已。

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

如果是 VB.Net,它应该被标记为单实例,如果是 C#,那么使用 Mutex 或者......我不知道该怎么做。

主窗体类:

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

【讨论】:

这怎么不是我建议的??【参考方案3】:

为什么不编写一个带有制作单实例应用程序检查的 .exe。

然后在那个新应用程序中捕获 MyApplication 类中提供的 MyApplication_StartupNextInstance 以捕获资源管理器推送的所有文件,也许让应用程序等待一两秒确保资源管理器没有发送以下文件,然后将它们全部组合成 1 个字符串并将它们解析到您的 3rd 方应用程序。

如果有兴趣,我可以放置一些代码让你开始

【讨论】:

感谢您的帮助,示例没问题,我已经看到了该事件的 event-args 所需的所有内容 :) 再次感谢。 对不起,我需要放弃这个解决方案,或者至少放弃我在回答中给出的重点,因为即使作为一个实例,如果我从资源管理器中选择/打开 100 个文件,它也会打开 100 个不同的实例,试图与第一个实例通信以传递新的命令行参数,然后用简单的话来说,这 100 个进程会使系统崩溃几秒钟,而且某些实例有时会抛出 CantStartSingleInstanceException 异常。它太可怕了:( 这不应该发生。如果您使您的应用程序单个实例只应打开 1 个应用程序,如果您的应用程序已经打开,则当前打开的应用程序会触发 MyApplication_StartupNextInstance 不,你的想法不对,我不是专家,但下一个实例应该打开/运行以与第一个实例通信......你可以做一个简单的测试自己证明这一点: 1)编译标记为单实例应用程序的 Vb.Net。 2) 打开 Windows 任务管理器。 3)在资源管理器中选择已编译的程序集并按住“ENTER”键,此时您可以注意到同一程序集的许多不同实例将打开并显示在任务管理器中,并在确定的时刻出现'CantStartSingleInstanceException ' 某些实例将引发异常。 我按照你的描述做了,我的应用程序只打开一次。不管按回车多少次,总是回到原来的exe

以上是关于实现 Explorer ContextMenu 并将多个文件传递给一个程序实例的主要内容,如果未能解决你的问题,请参考以下文章

ContextMenu的使用具体解释

ContextMenu的使用具体解释

WPF 之 左键弹出操作菜单,并禁用右键菜单

Android ContextMenu的使用

WPF:如何设置或禁用 TextBox 的默认 ContextMenu

macOS App SwiftUI 中的 ContextMenu 按钮点击