Folder chooser with edit box in hybrid batch script anyone?

Discussion in 'Scripting' started by Thomas Dubreuil, Mar 18, 2020.

  1. Thomas Dubreuil

    Thomas Dubreuil MDL Senior Member

    Aug 29, 2017
    363
    620
    10
    #1 Thomas Dubreuil, Mar 18, 2020
    Last edited: Mar 19, 2020
    I have a folder chooser dialog with edit box in my batch script, I chose BrowseForFolder method with Jscript because it is the fastest I tested.

    However, if I type a "non-existing path" or an "invalid name" in the edit box, it fails to pass the value back to the batch part, but instead pass default/root folder or last "explored" folder.

    Following the BROWSEINFO structure from https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-browseinfoa , I added the BIF_VALIDATE value (0x00000020) in the options, but I'm stuck here.
    I have no idea how I can hook BrowseCallbackProc and retrieve BFFM_VALIDATEFAILED message, or if it's even a possibility.
    I also tried folderbrowserdialog and other methods but it does not seem to have the edit box.

    Appart from that, it would be also great if :
    -Explorer box could follow the path typed/pasted in edit box, similarly to windows explorer/select folder.
    -I could browse above defined root folder: for example set a start folder, have that folder "expanded" with "ThisPC" still being the top folder.
    -There was a possibility to see full path in edit box when I browse (instead of folder name only)?

    Here is basic code example:
    Code:
    @if (@CodeSection == @Batch) @then
    @echo off
    
    :Installation_Browser
    echo Where do you want to install program?
    for /f "delims=" %%a in ('CScript //nologo //E:JScript "%~f0" "17"') do ( set "Install_Folder=%%a" )
    if "%Install_Folder%"=="" ( cls & exit /b )
    if "%Install_Folder%"=="::{20D04FE0-3AEA-1069-A2D8-08002B30309D}" ( cls & call :Wrong_Path_Error & goto :Installation_Browser )
    if not exist "%Install_Folder%" ( cls & call :Wrong_Path_Error & goto :Installation_Browser )
    echo "%Install_Folder%"
    REM robocopy "%Files_Path%" "%Install_Folder%" *.* /is /it /S >nul 2>&1
    
    :Shortcut_Browser
    echo Where do you want to create Start Menu shortcut?
    for /f "delims=" %%a in ('CScript //nologo //E:JScript "%~f0" "23"') do ( set "Shortcut_Folder=%%a" )
    if "%Shortcut_Folder%"=="" ( cls & exit /b )
    if "%Shortcut_Folder%"=="::{20D04FE0-3AEA-1069-A2D8-08002B30309D}" ( call :Wrong_Path_Error & goto :Shortcut_Browser )
    if not exist "%Shortcut_Folder%" ( call :Wrong_Path_Error & goto :Shortcut_Browser )
    echo "%Shortcut_Folder%"
    REM powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass "$s=(New-Object -COM WScript.Shell).CreateShortcut('%Shortcut_Folder%\Program.lnk');$s.TargetPath='%Install_Folder%\Program.exe';$s.WorkingDirectory='%Install_Folder%';$s.Description='This is just an example';$s.Save()" >nul 2>&1
    pause
    exit /b
    
    :Wrong_Path_Error
    echo The path does not exist.
    echo Choose an existing folder, or use "Make New Folder" button to create one.
    set "Install_Folder=" &set "Shortcut_Folder="
    goto :eof
    
    @end
    if (WScript.Arguments(0) == 17) {
        var Start_folder = 17;
    } else {
        var Start_folder = 23;
    }
    var Message = "Browse for location or paste an existing path in the message box below, then click OK.";
    var Box_style = 0x00000010 + 0x00000020 + 0x00000040 + 0x00010000;
    var shl = new ActiveXObject("Shell.Application");
    var folder = shl.BrowseForFolder(0, Message, Box_style, Start_folder );
    if (folder != null) {
        WScript.Stdout.WriteLine(folder ? folder.self.path : "");
    }
    
    Notes:
    I added ::{20D04FE0-3AEA-1069-A2D8-08002B30309D} for "This PC"
    if not exist "... doesn't work since it always pass the last "explored" path.

    for ref:
    0x00000010 is BIF_EDITBOX
    0x00000020 is BIF_VALIDATE
    0x00000040 is BIF_NEWDIALOGSTYLE (bigger and resizable window)
    0x00010000 follows file junctions

    I don't mind using C# or vbs or Jscript code, even Powershell but needs 2 seconds to load from batch, it just needs to be inside a batch.
    Open to suggestions, for the best user experience.
    Thanks!

    EDIT :
    I partially solved it using a C# project ported to Powershell.
    Code:
    <# : Batch portion
    @echo off
    set "Start_Folder=1"
    for /f "delims=" %%a in ('Powershell -nop -noni -c "iex (${%~f0} | out-string)"') do ( set "Program_Folder=%%a" )
    if "%Program_Folder%"=="" ( cls & exit /b )
    echo "%Program_Folder%"
    pause
    exit /b
    
    : end Batch portion / begin PowerShell hybrid chimera #>
    Function BuildDialog {
        $sourcecode = @"
    using System;
    using System.Windows.Forms;
    using System.Reflection;
    namespace FolderSelect
    {
        public class FolderSelectDialog
        {
            System.Windows.Forms.OpenFileDialog ofd = null;
            public FolderSelectDialog()
            {
                ofd = new System.Windows.Forms.OpenFileDialog();
                ofd.Filter = "Folders|\n";
                ofd.AddExtension = false;
                ofd.CheckFileExists = false;
                ofd.DereferenceLinks = true;
                ofd.Multiselect = false;
            }
            public string InitialDirectory
            {
                get { return ofd.InitialDirectory; }
                set { ofd.InitialDirectory = value == null || value.Length == 0 ? Environment.CurrentDirectory : value; }
            }
            public string Title
            {
                get { return ofd.Title; }
                set { ofd.Title = value == null ? "Select a folder" : value; }
            }
            public string FileName
            {
                get { return ofd.FileName; }
            }
            public bool ShowDialog()
            {
                return ShowDialog(IntPtr.Zero);
            }
            public bool ShowDialog(IntPtr hWndOwner)
            {
                bool flag = false;
                if (Environment.OSVersion.Version.Major >= 6)
                {
                    var r = new Reflector("System.Windows.Forms");
                    uint num = 0;
                    Type typeIFileDialog = r.GetType("FileDialogNative.IFileDialog");
                    object dialog = r.Call(ofd, "CreateVistaDialog");
                    r.Call(ofd, "OnBeforeVistaDialog", dialog);
                    uint options = (uint)r.CallAs(typeof(System.Windows.Forms.FileDialog), ofd, "GetOptions");
                    options |= (uint)r.GetEnum("FileDialogNative.FOS", "FOS_PICKFOLDERS");
                    r.CallAs(typeIFileDialog, dialog, "SetOptions", options);
                    object pfde = r.New("FileDialog.VistaDialogEvents", ofd);
                    object[] parameters = new object[] { pfde, num };
                    r.CallAs2(typeIFileDialog, dialog, "Advise", parameters);
                    num = (uint)parameters[1];
                    try
                    {
                        int num2 = (int)r.CallAs(typeIFileDialog, dialog, "Show", hWndOwner);
                        flag = 0 == num2;
                    }
                    finally
                    {
                        r.CallAs(typeIFileDialog, dialog, "Unadvise", num);
                        GC.KeepAlive(pfde);
                    }
                }
                else
                {
                    var fbd = new FolderBrowserDialog();
                    fbd.Description = this.Title;
                    fbd.SelectedPath = this.InitialDirectory;
                    fbd.ShowNewFolderButton = false;
                    if (fbd.ShowDialog(new WindowWrapper(hWndOwner)) != DialogResult.OK) return false;
                    ofd.FileName = fbd.SelectedPath;
                    flag = true;
                }
                return flag;
            }
        }
        public class WindowWrapper : System.Windows.Forms.IWin32Window
        {
            public WindowWrapper(IntPtr handle)
            {
                _hwnd = handle;
            }
            public IntPtr Handle
            {
                get { return _hwnd; }
            }
    
            private IntPtr _hwnd;
        }
        public class Reflector
        {
            string m_ns;
            Assembly m_asmb;
            public Reflector(string ns)
                : this(ns, ns)
            { }
            public Reflector(string an, string ns)
            {
                m_ns = ns;
                m_asmb = null;
                foreach (AssemblyName aN in Assembly.GetExecutingAssembly().GetReferencedAssemblies())
                {
                    if (aN.FullName.StartsWith(an))
                    {
                        m_asmb = Assembly.Load(aN);
                        break;
                    }
                }
            }
            public Type GetType(string typeName)
            {
                Type type = null;
                string[] names = typeName.Split('.');
                if (names.Length > 0)
                    type = m_asmb.GetType(m_ns + "." + names[0]);
    
                for (int i = 1; i < names.Length; ++i) {
                    type = type.GetNestedType(names[i], BindingFlags.NonPublic);
                }
                return type;
            }
            public object New(string name, params object[] parameters)
            {
                Type type = GetType(name);
                ConstructorInfo[] ctorInfos = type.GetConstructors();
                foreach (ConstructorInfo ci in ctorInfos) {
                    try {
                        return ci.Invoke(parameters);
                    } catch { }
                }
                return null;
            }
            public object Call(object obj, string func, params object[] parameters)
            {
                return Call2(obj, func, parameters);
            }
            public object Call2(object obj, string func, object[] parameters)
            {
                return CallAs2(obj.GetType(), obj, func, parameters);
            }
            public object CallAs(Type type, object obj, string func, params object[] parameters)
            {
                return CallAs2(type, obj, func, parameters);
            }
            public object CallAs2(Type type, object obj, string func, object[] parameters) {
                MethodInfo methInfo = type.GetMethod(func, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                return methInfo.Invoke(obj, parameters);
            }
            public object Get(object obj, string prop)
            {
                return GetAs(obj.GetType(), obj, prop);
            }
            public object GetAs(Type type, object obj, string prop) {
                PropertyInfo propInfo = type.GetProperty(prop, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                return propInfo.GetValue(obj, null);
            }
            public object GetEnum(string typeName, string name) {
                Type type = GetType(typeName);
                FieldInfo fieldInfo = type.GetField(name);
                return fieldInfo.GetValue(null);
            }
        }
    }
    "@
        $assemblies = ('System.Windows.Forms', 'System.Reflection')
        Add-Type -TypeDefinition $sourceCode -ReferencedAssemblies $assemblies -ErrorAction STOP
    }
        cd c: #THIS IS THE CRITICAL LINE
        BuildDialog
        $fsd = New-Object FolderSelect.FolderSelectDialog
        $fsd.Title = "Browse for location or paste an existing path in the message box below, then click Select Folder.";
        If ($env:Start_Folder -eq "1") {$fsd.InitialDirectory = "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}"};
        If ($env:Start_Folder -eq "2") {$fsd.InitialDirectory = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"};
        $fsd.ShowDialog() | Out-Null
        $fsd.FileName
    
    
    This "vista style" folder dialog is MUCH better. It works well in the script, but I'm facing 2 new problems :
    -Dialog is too slow to open (2-3 secs) due to powershell being called from batch, while Jscript dialog loaded instantly.
    -I can not embedd binary data with "ZeroMQ Base-85" encoding, because encoded text has <# #> characters = the same as in Powershell multi-line comments used in hybrid batch.
    I have to encode binaries in hex and so the batch file ends up being bigger.

    I guess the best way to solve those 2 pbs is to get rid of this "powershell chimera"...
    So, the 1million dollar question now is :
    Is there a way I can port this in my batch using only C# or another method ?
    Or any other solution/idea ?
    Someone here able to help ?
    Thanks
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  2. Tux 528

    Tux 528 MDL Novice

    Apr 20, 2020
    9
    11
    0
    Hi!

    You can perform all this using the "Bat To Exe Converter" software. It is very easy to use and you can easily compile your script into an executable and extend the possibilities of the Batch language with included commands and features, such as downloading a file, hiding a window or opening a dialog box to browse a folder. :animatedwink:

    Here is a download link: https://www.softpedia.com/get/System/File-Management/Batch-To-Exe-Converter.shtml

    Once the software is installed, simply use the "Extd" tool located in the options bar and create or modify your Batch script using the built-in editor.

    Then, you can compile it thanks to the right menu by selecting the desired options and saving the executable. Beware, it is possible that the compiled script is detected by antivirus software, but it is a false positive, don't worry.

    Best regards