Implement Explorer ContextMenu and pass multiple files to one program instance

后端 未结 3 852
别跟我提以往
别跟我提以往 2020-12-30 05:25

Situation

I have a 3rd party GUI application that accepts multiple files via CLI, for example:

MyProgram.exe \"file1\" \         


        
相关标签:
3条回答
  • 2020-12-30 05:35

    Why not write a .exe with Make single instance application checked.

    Then in that new application catch the MyApplication_StartupNextInstance as provided in the MyApplication class to catch all files being pushed by explorer, maybe let the application wait a second or 2 to make sure no following files are being sent by explorer and then combine those all into 1 string and parse them to your 3rd party app.

    If interested i can place some code getting you started

    0 讨论(0)
  • 2020-12-30 05:42

    EDIT: I discarded this solution because I've discovered that this approach has very bad disadvantages.


    So, this is how it looks in VB.Net this easy approach (thanks for @Roy van der Velde)

    It stores the filepaths in a string builder in this format:

    "File1" "File2 "File3"
    

    After an inactivity time (using a Timer), the filepath arguments are passed to the specified application, and that's all.

    The code is rehusable and customizable :)

    It should be marked as single-instance if VB.Net, if C# then use a Mutex or... I don't know how to.

    Main Form Class:

    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
    

    Application Events 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
    
    0 讨论(0)
  • 2020-12-30 05:44

    You Do want a ShellExtension

    What you want is not quite as simple as you think. The normal behavior for multiple file selections is to open each in a new Window/App instance. Actually, it just sends the selected files to the registered app and leaves it up to the app to decide how to work with them.

    There is at least 1 quick and easy alternative though:

    Method 1: Use Send-To

    Open the Send To folder ("C:\Users\YOURNAME\AppData\Roaming\Microsoft\Windows\SendTo") and add an entry for the app. The target would be the app you wish to feed/send file selection to:

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

    You don't need "%1" placeholders or anything else. You don't have to write an intermediary to do anything, just send the files directly to the actual app. It will work fine, as long as the app will accept more than one file on the command line.

    The only minor thing is that it resides on a "shared" or general sub menu rather than a top level context menu. It is also not "smart" in so far as it is available for any file extension unlike a proper ContextMenu handler, but it is a quick and easy, no-code solution which has been around for a long time.


    Method 2: Change the Verb Qualifier

    You can also change the verb qualifier/mode, which sounds like the easiest way. Take for instance, VideoLan's VLC player:

    If you click on multiple .MP4 files rather than open multiple instances, it opens with one of them and the rest are queued for play. This is done by modifying the verb in the registry:

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

    MultiSelectModel is a modifier for the Open verb:

    • Single for verbs that support only a single item
    • Player for verbs that support any number of items
    • Document for verbs which create a top level window for each item

    For my MediaProps applet, since it is concerned with the same file types, I piggybacked my verb onto the file types of VLC by adding a ViewProps verb which was set as MultiSelectModel.Player and generally worked in so far as my verbs did not confuse VLC.

    Unfortunately, there is still something amiss that I have not yet identified. Windows seems like it still is not gluing all the files together as expected - even if I make my own verbs. There is a step missing either in the registry config or with the app -- but with two other ways to do the same thing, I have never investigated further.


    Method 3: Create ShellExtension / ContextMenu Handler

    Many proposed solutions end up being a game of Whack-a-Mole where you have to fix the same 1 file-1 instance problem in an intervening app so it can feed concatenated arguments to the final actor. Since the end result is to have an Explorer ContextMenu to do something useful, lets just build a ShellExtension for this other application.

    This is easy because a framework is already done and available on CodeProject: How to Write Windows Shell Extension with .NET Languages. This is an MS-PL article complete with a finished ShellExtension project.

    With a few modifications, this will work perfectly to:

    • setup associations for multiple file types
    • collect multiple files clicked
    • format them into a command line arg set
    • pass the commandline to the actual worker app
    • provide a custom ContentMenu
    • display a snazzy menu icon

    The test bed for this is an applet to display the MediaInfo properties of media files (things like Duration, Frame Size, Codec, format etc). In addition to accepting Dropped files, it uses a ContextMenu DLL helper to accept multiple files selected in Explorer and feed them to the Single Instance display app.


    Very Important Note

    Since this was first posted, I have revised and updated the original MS-PL article making it much easier to use. The revision is also at CodeProject Explorer Shell Extensions in .NET (Revised) and still contains a VB and C# version.

    In the revised version, rather than having to make changes here and there, they are consolidated to a single block of variables. The article also explains why you might want to use the C# version, and provides links to articles explaining why it is not a good idea to use managed code for Shell Extensions.

    The 'model' remains that of a Shell Extension to simply launch a related app.

    The balance of this answer is still worth reading for the general concepts and background. It doesn't seem right to change it well after the fact even though much of the Code Changes section doesn't apply to the revision.


    1. Update the Assembly/Project Values

    For instance, I changed the assembly name to "MediaPropsShell". I also removed the root namespace but that is optional.

    Add a PNG icon of your choosing.

    Pick the appropriate platform. Since the original has 2 installers, you may have to specifically build an x86 version for a 32bit OS. AnyCPU works fine for 64bit OS, I'm not sure about x86. Most systems which use this model supply a 32 and 64 bit DLL for the shell extension helper, but most in the past could not be NET based either where AnyCPU is an option.

    Keep the target platform as NET 4. If you did not read the CodeProject article or have not researched this previously, this is important.

    2. Code changes

    As published on CodeProject, the handler also only passes one file and associates itself with only one file type. The code below implements the handler for multiple file types. You will also want to fix the menu names and so forth. All the changes are noted in the code below prefaces with {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
    

    Just below a bit needs to be changed in the IShellExtInit Members REGION as well:

    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
    

    The original code actually does have code for a multi file method which is commented out. I actually did not see it before adding one. The changed part is between the star strings.

    Also, it is sad to say, but with Option Strict, you will have to make 10 or so small changes to Microsoft's code. Just accept the changes IntelliSense suggests.


    Important Notes

    The model of a separate DLL to provide ContextMenu services on behalf of an EXE "engine" is very common. This is what all the xxxShell.DLL files are which you often see in folders along with program executables. The difference here is that you are building the DLL rather than the author of the app in question.

    1. All the changes except one are in the FileContextMenuExt class
    2. Be sure to change the GUID otherwise your handler could collide with others based on the same MS Template! There is a handy utility for this on your Tools menu.
    3. The BMP/PNG is optional
    4. The original MS version simply displayed the name of the file selected. So the relevant procedure is named OnVerbDisplayFileName. As you see, I did not change that. If you change it to match your actual operation, you will also need to change some references to it in the PInvoke heavy code for IContextMenu. Nobody but you will ever see that name though.
    5. A debug MessageBox is all that is there for the invoke action. You can see the actual code mine uses.

    The ReadMe in the original MS project describes this, but after you compile, copy the file to where it will reside and register it:

    regasm <asmfilename>.DLL /codebase
    

    To unregister:

    regasm <asmfilename>.DLL /unregister
    

    Use the RegAsm found in your Microsoft.NET\Framework64\v4.0.xxxx folder. This will have to be done from a Command Window with Administrator privileges (or the equivalent script). Alternatively for a deployed app, you can have the target app register/unregister the helper DLL using the Public Regster/UnRegister methods.


    Warning: make your code changes carefully and test things like loops and string formats before you compile; you want as few compile-test iterations as possible. The reason is that once you activate your new context menu, the DLL is in use by Explorer and cannot be replaced by a new build. You have to terminate the explorer.exe process (not just File Explorer!) to register and try a new build.

    There may be a another way, but I just close any Explorer Windows, then log off and right back on.


    Testing

    If I right click on one of the registered file types, I get the menu as expected with the correct menu text and bitmap image:

    enter image description here

    click for larger image

    If I click, the applet comes up as expected with multiple files in one instance:

    enter image description here

    click for larger image

    Note how the Prev/Next buttons at the bottom are enabled to move from file to file which is not the case when 1 file only is loaded.

    Works on My MachineTM


    Resources

    How to Write Windows Shell Extension with .NET Languages. This is an MS-PL article complete with a finished ShellExtension project. The above is a set of mods to make it work with multiple extensions and multiple files, so the original project is required as a starting point.

    Best Practices for Shortcut Menu Handlers and Multiple Verbs

    Choosing a Static or Dynamic Shortcut Menu Method

    Verbs and File Associations

    0 讨论(0)
提交回复
热议问题