Внедрить Explorer ContextMenu и передать несколько файлов в один экземпляр программы

Положение

У меня есть стороннее графическое приложение, которое принимает несколько файлов через CLI, например:

MyProgram.exe "file1" "file2"

Затем все файлы загружаются сразу в один и тот же экземпляр приложения.

Чтобы оптимизировать время, я хотел бы загрузить несколько файлов, выполнив щелчок правой кнопкой мыши по некоторым файлам из Проводника Windows (например: Выбрать 5 файлов > сделать правой кнопкой мыши > выбрать Команда "Открыть в программе MyProgram" )

Я знаю, как создать необходимые ключи реестра, чтобы добавить эту команду в контекстное меню для определенных типов файлов, что не является проблемой.

Проблема

Эта сторонняя программа не поставляется с каким-либо драйвером, расширением оболочки или методологией, которая может захватывать несколько файлов из контекстного меню, поэтому вместо этого Если я выберу 2 файла из проводника, каждый файл будет открыт в отдельном экземпляре программы, и у меня нет идеи разрабатывать драйверы, поэтому драйвер не является тем, что я ищу.

Фокус

Я открыт для предложений, возможно, это не эффективный способ, но кажется самым простым способом:

Моя идея - разработать приложение для мини-CLI, чтобы поймать эти несколько файлов (возможно, на основе сообщений Windows или бездействия SO, я не знаю, почему я спрашиваю), напишите эти файлы/аргументы в текстовом файле затем присоедините все аргументы в одной строке, чтобы вызвать мою стороннюю программу с этими аргументами, чтобы сразу загрузить все файлы в один экземпляр этой программы.

Другими словами, просто простой загрузчик, чтобы использовать его из контекстного меню при выборе нескольких файлов, чтобы сразу открывать все файлы в этом стороннем приложении.

Вопрос

Прежде всего, я хотел бы знать, существует ли известный термин для обозначения этой вещи приложения, способного загружать несколько файлов в один и тот же экземпляр, выбирая файлы из проводника, а затем contextmenu. Я хотел бы исследовать этот термин.

Какой может быть наиболее эффективный способ выполнить эту задачу в консольном приложении VB.NET/C#? (не драйвер)

Как начать развивать это?

Любой существующий пример исходного кода из известных страниц, таких как codeproject...?

Ответ 1

Вы хотите ShellExtension

То, что вы хотите, не так просто, как вы думаете. Обычное поведение для множественного выбора файлов заключается в открытии каждого из них в новом экземпляре Window/App. Фактически, он просто отправляет выбранные файлы в зарегистрированное приложение и оставляет его в приложении, чтобы решить, как с ними работать.

Существует, по крайней мере, 1 быстрая и простая альтернатива:

Способ 1: Использовать Send-To

Откройте папку 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"

MultiSelectModel является модификатором для глагола Open:

  • Одиночный для глаголов, поддерживающих только один элемент
  • Player для глаголов, поддерживающих любое количество элементов
  • Документ для глаголов, которые создают окно верхнего уровня для каждого элемента

Для моего апплета MediaProps, поскольку он касается одинаковых типов файлов, я включил свой глагол в типы файлов VLC, добавив глагол ViewProps, который был установлен как MultiSelectModel.Player и, как правило, работал в той мере, глаголы не путают VLC.

К сожалению, есть еще что-то нехорошее, что я еще не определил. Кажется, что Windows по-прежнему не склеивает все файлы вместе, как и ожидалось, даже если я создаю свои собственные глаголы. В конфигурации реестра или с приложением отсутствует шаг, но с двумя другими способами сделать то же самое, я никогда не исследовал дальше.


Способ 3: Создать обработчик ShellExtension/ContextMenu

Многие предлагаемые решения в конечном итоге являются игрой Whack-a-Mole, где вы должны исправить то же самое 1 проблема экземпляра файла-1 в промежуточном приложении, чтобы он мог передавать конкатенированные аргументы конечному игроку. Поскольку конечный результат состоит в том, чтобы Explorer ContextMenu сделать что-то полезное, просто создайте ShellExtension для этого другого приложения.

Это легко, потому что фреймворк уже сделан и доступен в CodeProject: Как написать расширение Windows Shell с .NET-языками. Это статья MS-PL с готовым проектом ShellExtension.

С помощью нескольких модификаций это будет отлично работать:

  • установочные ассоциации для нескольких типов файлов
  • собрать несколько файлов, нажатых
  • отформатировать их в командной строке arg set
  • передать командную строку фактическому рабочему приложению
  • предоставить пользовательский ContentMenu
  • отображает значки меню

Тест-кровать для этого является апплетом для отображения свойств MediaInfo медиафайлов (таких как Duration, Frame Size, Codec, format и т.д.). В дополнение к принятию Dropped файлов он использует помощник DLL ContextMenu для приема нескольких файлов, выбранных в Проводнике, и передает их в приложение отображения Single Instance.


Очень важное примечание

Поскольку это было впервые опубликовано, у меня есть переработанная и обновленная оригинальная статья MS-PL, что делает ее намного проще в использовании. Пересмотр также находится в CodeProject Расширения оболочки проводника в .NET(пересмотренный) и содержит версию VB и С#.

В пересмотренной версии вместо того, чтобы делать изменения здесь и там, они объединяются в один блок переменных. В статье также объясняется, почему вы можете использовать версию С# и ссылаетесь на статьи, объясняющие, почему не рекомендуется использовать управляемый код для расширений оболочки.

"Модель" остается для расширения оболочки, чтобы просто запустить соответствующее приложение.

Баланс этого ответа по-прежнему стоит прочитать для общих концепций и фона. Не кажется правильным изменить его после факта, даже если большая часть раздела Изменения кода не применяется к ревизии.


1. Обновить значения сборки/проекта

Например, я изменил имя сборки на "MediaPropsShell". Я также удалил корневое пространство имен, но это необязательно.

Добавьте значок PNG по вашему выбору.

Выберите подходящую платформу.. Поскольку в оригинале есть 2 установщика, вам, возможно, придется специально построить версию x86 для 32-разрядной ОС. AnyCPU отлично работает для 64-битной ОС, я не уверен в x86. Большинство систем, которые используют эту модель, предоставляют 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 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 вам придется внести 10 или около того небольших изменений в код Microsoft. Просто примите изменения, предлагаемые IntelliSense.


Важные примечания

Модель отдельной DLL для предоставления услуг ContextMenu от имени EXE-движка " очень является общей. Это все файлы xxxShell.DLL, которые вы часто видите в папках вместе с исполняемыми программами. Разница здесь в том, что вы создаете DLL, а не автор приложения.

  • Все изменения, кроме одного, находятся в классе FileContextMenuExt
  • Обязательно измените GUID, иначе ваш обработчик может столкнуться с другими на основе того же шаблона MS! В вашем меню Tools есть удобная утилита.
  • BMP/PNG необязательно
  • Оригинальная версия MS просто отображает имя выбранного файла. Соответственно, соответствующая процедура называется OnVerbDisplayFileName. Как вы видите, я не изменил этого. Если вы измените его в соответствии с фактической операцией, вам также нужно будет изменить некоторые ссылки на него в тяжелом коде PInvoke для IContextMenu. Никто, кроме вас, никогда не увидит это имя.
  • Отладка MessageBox - это все, что есть для действия invoke. Вы можете видеть, как используется мой код.

ReadMe в исходном проекте MS описывает это, но после компиляции скопируйте файл туда, где он будет находиться, и зарегистрируйте его:

regasm <asmfilename>.DLL /codebase

Отменить регистрацию:

regasm <asmfilename>.DLL /unregister

Используйте RegAsm, найденный в вашей папке Microsoft.NET\Framework64\v4.0.xxxx. Это нужно сделать из окна команд с правами администратора (или эквивалентом script). В качестве альтернативы для развернутого приложения вы можете настроить целевое приложение для регистрации/отмены регистрации вспомогательной DLL с помощью методов Public Regster/UnRegister.


Предупреждение: тщательно измените свой код и проверите вещи, такие как циклы и строковые форматы , прежде чем компилировать; вы хотите как можно меньше итераций для компиляции. Причина в том, что после активации нового контекстного меню DLL используется Explorer и не может быть заменена новой сборкой. Вам нужно завершить процесс explorer.exe (а не только Проводник!) Для регистрации и попробовать новую сборку.

Может быть другой способ, но я просто закрываю любую проводку Windows, а затем выхожу и снова включаю.


Тестирование

Если я нажимаю правой кнопкой мыши на один из зарегистрированных типов файлов, я получаю меню, как ожидалось, с правильным текстом меню и растровым изображением:

enter image description here

нажмите для увеличения изображения

Если я нажму, апплет появится как ожидалось с несколькими файлами в одном экземпляре:

enter image description hereвведите описание изображения здесь

нажмите для увеличения изображения

Обратите внимание, что кнопки Prev/Next внизу позволяют перемещаться из файла в файл, что не соответствует загрузке только одного файла.

Работает на моей машине TM


Ресурсы

Как написать расширение Windows Shell с языками .NET. Это статья MS-PL с готовым проектом ShellExtension. Вышеупомянутый набор модов, чтобы он работал с несколькими расширениями и несколькими файлами, поэтому исходный проект требуется в качестве отправной точки.

Рекомендации для ярлыков меню и несколько глаголов

Выбор метода статического или динамического контекстного меню

Глаголы и ассоциации файлов

Ответ 2

Почему бы не написать файл .exe с отметкой Сделать одиночное приложение экземпляра.

Затем в этом новом приложении поймайте MyApplication_StartupNextInstance, как показано в классе MyApplication, чтобы поймать все файлы, которые выталкивает проводник, возможно, пусть приложение ждет секунду или 2 убедитесь, что ни один из следующих файлов не отправляется проводником, а затем объединяет все в 1 строку и анализирует их на стороннем приложении.

Если интересно, я могу разместить код, который вы начали.

Ответ 3

EDIT: я отбросил это решение, потому что обнаружил, что этот подход имеет очень плохие недостатки.


Итак, так выглядит в VB.Net этот простой подход (спасибо за @ Рой ван дер Вельде)

Он хранит пути к файлам в построителе строк в этом формате:

"File1" "File2 "File3"

После времени бездействия (с помощью Таймера) аргументы filepath передаются в указанное приложение и все.

Код может быть изменен и настраиваться:)

Он должен быть помечен как один экземпляр, если VB.Net, если С# затем использует 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