实现资源管理器右键菜单并将多个文件传递给一个程序实例

24

情况

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

MyProgram.exe "file1" "file2"

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

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

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

问题

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

焦点

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

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

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

问题

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

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

如何开始开发?

是否有任何已知页面(例如codeproject)中的现有源代码示例?


1
你要查找的搜索词是“.net 4资源管理器上下文菜单”。 - Andrew Morton
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - ElektroStudios
2
我必须让我的Google训练得很好,因为找到的第一项是.NET Shell Extensions - Shell Context Menus - Andrew Morton
是的,正如我所说的,这并不是什么有趣的东西,或者至少我无法感知是否这就是我真正需要完成这个任务的东西(如果是这样,对我的无知表示抱歉),因为在那个例子中似乎只是讲述了如何将项目添加/注册到上下文菜单。谢谢。 - ElektroStudios
1
可能有助于了解该解决方案应该与哪个应用程序/实用工具配合使用。 - Ňɏssa Pøngjǣrdenlarp
显示剩余6条评论
3个回答

46

您确实需要一个ShellExtension

您所需的并不像您想象的那么简单。多个文件选择的正常行为是在新窗口/应用程序实例中打开每个文件。实际上,它只是将所选文件发送到已注册的应用程序,并由应用程序决定如何处理它们。

但至少有1种快速简便的替代方法:

方法1:使用“发送到”

打开“发送到”文件夹("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适用于仅支持单个项目的动词
  • Player适用于支持任意数量项目的动词
  • Document适用于为每个项目创建顶级窗口的动词

对于我的MediaProps小程序,由于它涉及相同的文件类型,我通过添加一个设置为MultiSelectModel.PlayerViewProps动词来将我的动词附加到VLC的文件类型上,并且通常运行良好,以至于我的动词不会使VLC出现混乱。

不幸的是,仍然有一些问题我还没有找到。似乎Windows仍然没有像预期的那样将所有文件粘合在一起-即使我自己创建了动词。注册表配置或应用中可能缺少一步-但是由于还有另外两种完成相同任务的方法,所以我从未进一步调查过。


方法三:创建ShellExtension / 上下文菜单处理程序

许多提出的解决方案最终都变成了一个打地鼠游戏,您必须修复介入应用程序中的相同1文件-1实例问题,以便它可以将连接的参数传递给最终的操作者。由于最终结果是拥有一个资源管理器上下文菜单来执行一些有用的操作,因此我们只需为这个其他应用程序构建一个ShellExtension

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

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

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

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


非常重要的提示

自首次发布以来,我已经修订和更新了原始的MS-PL文章,使其更易于使用。修订版也在CodeProject上.NET中的资源管理器Shell扩展(修订版),仍包含VB和C#版本。

在修订版中,不必到处更改,而是将它们合并为单个变量块。该文章还解释了为什么您可能想要使用C#版本,并提供了解释为什么使用托管代码作为Shell扩展的不好的想法的文章链接。

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

本答案的余下部分仍值得阅读,以了解一般概念和背景。即使很多代码更改部分不适用于修订版,但事实上修改它似乎不太合适。


1. 更新程序集/项目数值

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

添加您选择的PNG图标。

选择适当的平台。 由于原始版本有2个安装程序,您可能必须专门为32位操作系统构建x86版本。AnyCPU对64位操作系统有效,但我不确定对于x86是否也适用。大多数使用此模型的系统都会提供一个32位和64位的DLL作为shell扩展助手,但过去大多数不能基于.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 成员 区域下面,还需要更改一点内容:

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建议的更改即可。

重要说明

提供ContextMenu服务的独立DLL模型代表一个EXE“引擎”的常见方法。这就是您经常在程序可执行文件所在文件夹中看到的所有xxxShell.DLL文件。不同之处在于,正在构建DLL,而不是相关应用程序的作者。

  1. 除一个之外的所有更改都在FileContextMenuExt类中进行。
  2. 请务必更改GUID,否则您的处理程序可能会与基于相同MS模板的其他处理程序冲突!您的工具菜单上有一个方便的实用程序可供使用。
  3. BMP/PNG是可选的。
  4. 最初的MS版本只显示所选文件的名称。因此,相关过程的名称为OnVerbDisplayFileName。如您所见,我没有更改它。如果您将其更改以匹配实际操作,则还需要更改IContextMenu的PInvoke代码中对其的一些引用。不过,除了您之外,没有人会看到该名称。
  5. 调试MessageBox是调用操作中存在的所有内容。您可以查看我的实际代码。

原始的MS项目中的ReadMe文件描述了这一点,但是在编译后,将该文件复制到它所在的位置并注册:

regasm <asmfilename>.DLL /codebase

取消注册:
regasm <asmfilename>.DLL /unregister

使用位于 Microsoft.NET\Framework64\v4.0.xxxx 文件夹中的 RegAsm。这将需要在管理员权限的命令窗口(或等效脚本)中完成。另外,对于已部署的应用程序,您可以使用 Public Regster/UnRegister 方法让目标应用程序注册/注销辅助 DLL。
警告:在编译之前,务必仔细修改您的代码,并测试循环和字符串格式等内容;您希望尽可能少地进行编译测试迭代。原因是一旦激活新的上下文菜单,DLL 就被资源管理器使用,不能被新构建替换。您必须终止 explorer.exe 进程(不仅仅是文件资源管理器!)才能注册并尝试新的构建。
也许还有另一种方法,但我只是关闭任何资源管理器窗口,然后注销并立即重新登录。

测试

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

enter image description here

点击查看更大的图片

如果我点击,小程序会按预期带有多个文件的一个实例弹出:

enter image description hereenter image description here

点击查看大图

请注意底部的“上一张/下一张”按钮已启用,可以在多个文件之间移动,而当仅加载1个文件时,情况并非如此。

在我的机器上有效TM


资源

如何使用.NET语言编写Windows Shell扩展。这是一个带有完成的ShellExtension项目的MS-PL文章。上述是一组修改,使其能够处理多个扩展和多个文件,因此原始项目是必需的起点。

快捷菜单处理程序和多个动作的最佳实践

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

动作和文件关联


哇!多么好的答案,谢谢!“sendto”方法给了我一个想法,也许可以从应用程序内部调用“sendto”功能,比如使用P/Invoking或其他方式?你觉得这种方法怎么样?它能像“sendto”功能一样正常工作吗?:·创建一个上下文菜单来调用一个CLI应用程序,该应用程序将逐个捕获所有选定的文件以使用“sendto”功能,对每个文件或所有文件一次性发送到我的程序。你认为这可行并且能够按预期工作吗? - ElektroStudios
1
通过PInvoke发送到似乎像是一个临时解决方案,可能会导致更多的Whack-a-Mole问题,特别是当一个适当的ContextMenu助手如此容易实现时。请查看更大的图像 - 它使用了一个透明的PNG,尽管透明区域非常小。 - Ňɏssa Pøngjǣrdenlarp
1
我为我一直在开发的项目实现了“发送到”选项,但是在发布后开始收到大量投诉,称用户在弹出窗口中报告了“文件名或扩展名太长”的异常。果然,“发送到”方法受到 shell 的 32k 环境限制(其中包括由链接执行的命令)。有没有什么想法或想法可以解决这个问题,而不需要实现完整的 ShellExtension 解决方案? - tpartee
有趣。我会尝试使用资源管理器上下文菜单。你还可以配置应用程序,使得当用户在应用程序已经打开的情况下使用菜单时,已经运行的应用程序做出响应。 - Ňɏssa Pøngjǣrdenlarp
@Plutonix,抱歉我没太明白你说的话,不过我能实现我的目标吗(将选定的文件添加到我的音乐播放器中)?请看一下这个链接 - ACE
显示剩余3条评论

3
请问是否考虑勾选“创建单实例应用程序”选项后,编写一个 .exe 文件。然后在新的应用程序中,通过 MyApplication 类提供的 MyApplication_StartupNextInstance 方法来捕获由资源管理器推送的所有文件,或许可以让应用程序等待一两秒钟,以确保资源管理器没有再发送任何文件,然后将这些文件合并为一个字符串并解析到第三方应用程序中呢?
如果您有兴趣,我可以提供一些帮助您入门的代码。

这不应该发生。如果您将应用程序设置为单实例,则只会打开1个应用程序,如果您的应用程序已经打开,则当前打开的应用程序会触发MyApplication_StartupNextInstance。 - Roy van der Velde
1
不,你的想法是错误的,我并不是专家,但下一个实例应该打开/运行以与第一个实例通信... 你可以自己进行简单易行的测试来证明这一点:1)编译标记为单实例应用程序的Vb.Net。2)打开Windows任务管理器。3)在资源管理器中选择已编译的程序集并按住“ENTER”键,在此时你会注意到许多不同的相同程序集实例将被打开并显示在任务管理器中,并且在某个确定的时刻,一些实例将抛出'CantStartSingleInstanceException'异常。 - ElektroStudios
我不是专家,但下面的实例应该打开/运行以与第一个实例通信。事实并非如此,.NET框架会检查您的应用程序是否正在运行,并直接与正在运行的应用程序通信,而不是首先打开一个新的应用程序! - Roy van der Velde
听起来你没有正确设置单一实例。如果正确设置,当应用程序已在后台运行时,不应该执行其他实例! - Roy van der Velde
在VB.Net Winforms项目中,设置单实例只有一种(自动化)方式,我正确地检查了选项,如果我选择10-16个文件之间,它可以正常工作(但在选择期间会打开这些10-16个实例),但是当我选择更多的文件时,就越有可能看到那个异常,并且无论如何,所有这些实例都会使系统崩溃几秒钟。 - ElektroStudios
显示剩余7条评论

2

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


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

它以以下格式将文件路径存储在一个字符串构建器中:

"File1" "File2 "File3"
使用计时器(Timer)检测空闲时间后,将文件路径参数传递给指定的应用程序,就是这样。 此代码可重复使用和自定义 :)
如果使用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

这不是我建议的方式吗? - Roy van der Velde

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接