Jump to content

Accept Multiple Files (UDF) from Windows Explorer context menu


orbs
 Share

Recommended Posts

Accept Multiple Files UDF is inspired by this topic and this topic.

 

Introduction

if your app (compiled script) is launched from Windows Explorer context menu, then you've probably noticed that when multiple files are selected, only one file is passed to your app, so Windows starts multiple instances of your app, one for each file selected.

few simple ways to overcome this issue are:

1) put a shortcut to your app on your "Send To..." context menu. this passes all the selected files as individual parameters to a single instance of your app.

2) put a shortcut to your app somewhere accessible, and drag the selected files to that shortcut. that will give you the same effect. (an accessible location might not include the task bar, unfortunately. but the Start menu for example is ok).

3) use an intermediate launcher, like this one (not tested by me).

if none of these apply to you, then this UDF allows your app to handle this issue internally.

 

Usage - yes, it is a one-liner:

Global $aFiles = _AcceptMultipleFiles()

 

Example Script - with comprehensive comments inside:

#AutoIt3Wrapper_Au3Check_Parameters=-q -d -w 1 -w 2 -w 3 -w 4 -w 5 -w 6 -w 7
#include <Array.au3> ; this is needed only For the _ArrayDisplay later In this example script.
#include 'AMF.au3' ; include the "Accept Multiple Files" UDF.
#NoTrayIcon ; disable this interactive feature.

Global $hTimer = TimerInit() ; for testing purposes
Global $aFiles = _AcceptMultipleFiles()
Global $fDiff = Round(TimerDiff($hTimer)) / 1000 ; determine how long the process took
#cs
calling _AcceptMultipleFiles makes the calling script accept all files passed as parameters to multiple instances of the calling script,
typically invoked by Windows Explorer context menu. list of files is returned as an array.

this is best performed as early as possible in your script, and definitely before any user interaction.
it will either make this instance the 'master', that will accept multiple files to process (and continue running),
or submit its file to the master instance if one is already active (and exit).

*NOTE: if there are files to process, and if this is NOT the 'master' instance, then this line will never be reached.

*NOTE: if only one file is selected - thus only one instance is invoked - then that instance becomes the 'master'
and the files array contains only the one file.

if your code made it this far, there are two possible scenarios:
  1) there are files to process, and they are stored in the array. element zero and @extended both store the number of files.
  2) your app was called with no parameters, or too many, or one that is not a file. in this case element zero = 0 and @extended = 0.
hence, you can use element zero to decide on your next action. something like this:
#ce
If $aFiles[0] > 0 Then
    ; <<<<<<<<<< do your stuff with the files listed in $aFiles >>>>>>>>>>
    _ArrayDisplay($aFiles, 'AMF Example (' & $fDiff & ' sec)')
Else
    ; <<<<<<<<<< do your stuff when no parameters were specified, or too many parameters, or a single parameter which is not a valid file >>>>>>>>>>
    MsgBox(0, 'AMF Example (' & $fDiff & ' sec)', 'This was the command line:' & @CRLF & $CmdLineRaw)
EndIf

 

UDF:

#include-Once

; #INDEX# =======================================================================================================================
; Title .........: Accept Multiple Files
; AutoIt Version : 3.3.16.1
; UDF Version ...: 2.0
; Status ........: Production
; Language ......: English
; Description ...: Allows a script to accept multiple files from Windows Explorer context menu, which invokes multiple instances.
;                  One instance becomes the 'master'. All other instanes submit their files to the master and exit, so only the
;                  'master' instance remains active and can process all files.
; Author(s) .....: orbs
; ===============================================================================================================================

; #VARIABLES# ===================================================================================================================
Global $__g_AMF_bDebug = False
; ===============================================================================================================================

; #CURRENT# =====================================================================================================================
; _AcceptMultipleFiles
; ===============================================================================================================================

; #FUNCTION# ====================================================================================================================
; Name ..........: _AcceptMultipleFiles
; Description ...: Accepts multiple files given as parameters to multiple instances of the calling script.
; Syntax ........: _AcceptMultipleFiles([$iAttempts = 10[, $iInterval = 10[, $bDebug = False]]])
; Parameters ....: $iAttempts  - [optional] number of attempts of searching for submitted files. Default (and minimum) is 10.
;                  $iInterval  - [optional] number of milliseconds between attempts. Default (and minimum) is 10.
;                  $bDebug     - [optional] default is False. if True, log files are created in the temporary folder.
; Return values .: a 1-based array of all files submitted by all instances. @extended is set to file count.
; Author ........: orbs
; Modified ......:
; Remarks .......: - if no parameters were specified, or too many parameters, or a single parameter which is not a valid file,
;                    then the function returns an array with element [0]=0, and @extended is set to 0.
;                  - to improve reliability, the calling script may increase $iAttempts and/or $iInterval.
; Related .......:
; Link ..........:
; Example .......: Yes
; ===============================================================================================================================
Func _AcceptMultipleFiles($iAttempts = 10, $iInterval = 10, $bDebug = False)
    ; process parameters
    If ($iAttempts = Default) Or ($iAttempts < 10) Then $iAttempts = 10
    If ($iInterval = Default) Or ($iInterval < 10) Then $iInterval = 10
    If $bDebug = Default Then $bDebug = False
    $__g_AMF_bDebug = $bDebug
    __AMF_Log('enter _AcceptMultipleFiles')
    __AMF_Log('attempts: ' & $iAttempts)
    __AMF_Log('interval: ' & $iInterval)
    ; initialize return data
    Local $aFiles[1] = [0]
    ; validate parameter
    If Not ($CmdLine[0] = 1 And FileExists($CmdLine[1])) Then Return SetExtended(__AMF_Log('invalid param => return empty array'), $aFiles)
    ; prepare variables and parent (root) folder
    Local Const $sAMF_Root = @LocalAppDataDir & '\AMF'
    __AMF_Log('AMF root: ' & $sAMF_Root)
    __AMF_Log('root DirCreate = ' & DirCreate($sAMF_Root))
    Local Const $sAMF_Path = @LocalAppDataDir & '\AMF\' & __AMF_CreateGUID(StringRight(@ScriptFullPath, 16))
    Local Const $sAMF_MasterFile = $sAMF_Path & '\master'
    __AMF_Log('AMF path: ' & $sAMF_Path)
    __AMF_Log('master file: ' & $sAMF_MasterFile)
    Local $iIteration = 0
    Local $bFound
    Local $hSearch
    Local $sAMF_File
    Local $sFile
    ; determine if master
    While Not __AMF_TakeOver($sAMF_Path, $sAMF_MasterFile) ; see if this should become the master process.
        If __AMF_MasterIsActive($sAMF_MasterFile, $iAttempts, $iInterval) Then ;; if not, then check if there is already an active master process.
            __AMF_SubmitFile($sAMF_Path, $CmdLine[1]) ;;; if so, then submit the file to the master and exit.
            __AMF_Log('file submitted => exit')
            Exit
        Else
            __AMF_Cleanup($sAMF_Path) ;; if there is no active master, assume the past master has crashed. cleanup and repeat trying to take over.
        EndIf
    WEnd
    __AMF_Log('this instance is master')
    ; start with first file, submitted by the master
    ReDim $aFiles[UBound($aFiles) + 1]
    $aFiles[0] += 1
    $aFiles[UBound($aFiles) - 1] = $CmdLine[1]
    ; process submitted files
    __AMF_Log('searching for submitted files')
    Do
        $iIteration += 1
        $bFound = False
        __AMF_Log('start iteration #' & $iIteration)
        For $i = 1 To $iAttempts
            __AMF_Log('attempt #' & $i)
            FileClose($hSearch)
            $hSearch = FileFindFirstFile($sAMF_Path & '\PID_*')
            If @error Then
                __AMF_Log('FileFindFirstFile @error=' & @error & ' => next attempt')
                FileClose($hSearch)
                Sleep($iInterval)
                ContinueLoop
            EndIf
            $sAMF_File = FileFindNextFile($hSearch)
            If @error Then
                __AMF_Log('FileFindNextFile @error=' & @error & ' => next attempt')
                Sleep($iInterval)
                ContinueLoop
            EndIf
            $bFound = True
            __AMF_Log('$sAMF_File = ' & $sAMF_File)
            $sAMF_File = $sAMF_Path & '\' & $sAMF_File
            __AMF_Log('prefix full path = ' & $sAMF_File)
            __AMF_Log('FileGetAttrib = ' & FileGetAttrib($sAMF_File))
            If StringInStr(FileGetAttrib($sAMF_File), 'R') Then
                __AMF_Log('Attrib R found')
                $sFile = FileReadLine($sAMF_File)
                __AMF_Log('FileReadLine = ' & $sFile)
                If $sFile = '' Then
                    __AMF_Log('result is empty => repeat attempt')
                    Sleep($iInterval)
                    $i -= 1
                    ContinueLoop
                Else
                    __AMF_Log('FileSetAttrib -R = ' & FileSetAttrib($sAMF_File, '-R'))
                    __AMF_Log('FileDelete = ' & FileDelete($sAMF_File))
                EndIf
            Else
                __AMF_Log('Attrib R not found => repeat attempt')
                Sleep($iInterval)
                $i -= 1
                ContinueLoop
            EndIf
            ReDim $aFiles[UBound($aFiles) + 1]
            $aFiles[0] += 1
            $aFiles[UBound($aFiles) - 1] = $sFile
            __AMF_Log('added to array => repeat attempt on next file')
            $i -= 1
        Next
        __AMF_Log('iteration ended. files found = ' & $bFound)
    Until Not $bFound
    FileClose($hSearch)
    __AMF_Log('search for submitted files ended')
    __AMF_Cleanup($sAMF_Path)
    Return SetExtended(__AMF_Log('end _AcceptMultipleFiles => return array with @extended=' & $aFiles[0], $aFiles[0]), $aFiles)
EndFunc   ;==>_AcceptMultipleFiles

; #INTERNAL_USE_ONLY# ===========================================================================================================
; __AMF_CreateGUID
; __AMF_TakeOver
; __AMF_MasterIsActive
; __AMF_SubmitFile
; __AMF_Cleanup
; __AMF_Log
; ===============================================================================================================================

Func __AMF_CreateGUID($sString)
    ; ref: https://www.autoitscript.com/forum/topic/147995-createguidfromstring-convert-a-string-to-a-valid-guid/
    Return StringRegExpReplace(StringToBinary($sString) & "0000000000000000000000000000000000", "..(.{8})(.{4})(.{4})(.{4})(.{12}).*", "\{$1-$2-$3-$4-$5\}")
EndFunc   ;==>__AMF_CreateGUID

Func __AMF_TakeOver($sAMF_Path, $sAMF_MasterFile)
    __AMF_Log('enter __AMF_TakeOver')
    Local $aResult = DllCall('kernel32.dll', 'bool', 'CreateDirectoryW', 'wstr', $sAMF_Path, 'struct*', 0)
    If @error Or ($aResult[0] = 0) Then
        Return __AMF_Log('cannot create folder => this instance is not the master. return 0')
    Else
        __AMF_Log('FileWriteLine = ' & FileWriteLine($sAMF_MasterFile, @AutoItPID))
        __AMF_Log('FileSetAttrib +R = ' & FileSetAttrib($sAMF_MasterFile, '+R'))
        Return __AMF_Log('master file created => return 1', 1)
    EndIf
EndFunc   ;==>__AMF_TakeOver

Func __AMF_MasterIsActive($sAMF_MasterFile, $iAttempts, $iInterval)
    __AMF_Log('enter __AMF_MasterIsActive')
    Local $iPID
    For $i = 1 To $iAttempts
        If StringInStr(FileGetAttrib($sAMF_MasterFile), 'R') Then
            $iPID = Number(FileReadLine($sAMF_MasterFile))
            ExitLoop
        Else
            Sleep($iInterval)
        EndIf
    Next
    __AMF_Log('read PID = ' & $iPID)
    If $iPID = 0 Then Return __AMF_Log('PID=0 => return 0')
    If ProcessExists($iPID) Then Return __AMF_Log('process exists => return 1', 1)
    Return __AMF_Log('end __AMF_MasterIsActive => return 0')
EndFunc   ;==>__AMF_MasterIsActive

Func __AMF_SubmitFile($sAMF_Path, $sFile)
    __AMF_Log('enter __AMF_SubmitFile')
    Local $sAMF_File = $sAMF_Path & '\PID_' & @AutoItPID
    __AMF_Log('FileDelete = ' & FileDelete($sAMF_File))
    __AMF_Log('file to submit: ' & $sFile)
    __AMF_Log('FileWriteLine = ' & FileWriteLine($sAMF_File, $sFile))
    __AMF_Log('FileSetAttrib +R = ' & FileSetAttrib($sAMF_File, 'R'))
    __AMF_Log('end __AMF_SubmitFile')
EndFunc   ;==>__AMF_SubmitFile

Func __AMF_Cleanup($sAMF_Path)
    __AMF_Log('enter __AMF_Cleanup')
    __AMF_Log('dir to remove: ' & $sAMF_Path)
    __AMF_Log('FileSetAttrib -R = ' & FileSetAttrib($sAMF_Path, '-R', 1))
    __AMF_Log('DirRemove = ' & DirRemove($sAMF_Path, 1))
    __AMF_Log('end __AMF_Cleanup')
EndFunc   ;==>__AMF_Cleanup

Func __AMF_Log($sString, $xRet = 0)
    If $__g_AMF_bDebug Then FileWriteLine(@TempDir & '\AMF_log_' & @AutoItPID & '.txt', @HOUR & ':' & @MIN & ':' & @SEC & '.' & @MSEC & ' (' & @AutoItPID & ') ' & $sString)
    Return $xRet
EndFunc   ;==>__AMF_Log

 

Testing Instructions:

1) save the UDF as file "AMF.au3".

2) save the example script as file "AMF_Example.au3".

3) compile the example script as "AMF_Example.exe".

4) place the executable in the root of drive D (or anywhere else ,but adapt the following registry entry accordingly)

5) save this text as registry file "AMF_Example.reg" and import it:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\*\shell\AMF Example]
"MultiSelectModel"="Player"

[HKEY_CLASSES_ROOT\*\shell\AMF Example\command]
@="D:\\AMF_Example.exe \"%1\""

6) open Windows Explorer, select multiple files, right click one of them and choose "AMF Example" from the context menu.

7) enjoy!

Signature - my forum contributions:

Spoiler

UDF:

LFN - support for long file names (over 260 characters)

InputImpose - impose valid characters in an input control

TimeConvert - convert UTC to/from local time and/or reformat the string representation

AMF - accept multiple files from Windows Explorer context menu

DateDuration -  literal description of the difference between given dates

Apps:

Touch - set the "modified" timestamp of a file to current time

Show For Files - tray menu to show/hide files extensions, hidden & system files, and selection checkboxes

SPDiff - Single-Pane Text Diff

 

Link to comment
Share on other sites

  • 6 months later...

I like this option but can't seem to get it to work.  Maybe since I'm using Windows 11.

I created 3 text files  and when I select all 3 files, I right click on any of them and choose the new context menu option. It asks me which application to open it in multiple times.

I was creating a tiny app to right click on multiple files and have them zip them with my own password.  Currently using my app during testing it to an array as it seems you are doing, it opens my array up 3 times (1 instance for each file selected)

I was hoping your solution would just open a single instance of an array with all 3 files listed. 😕

Link to comment
Share on other sites

  • 6 months later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...