How to set the directory of a save as dialog box through code?

1.2k views Asked by At

Basically I've written some code that listens for a "save as" dialog box to pop up inside an application, and when it does it presses "save", all through code. This works great, however I need to be able to set the file path to what I'd like before saving.

Here is my code so far:

using System;
using System.Diagnostics;
using System.ComponentModel;
using System.Collections.Generic;



using System.Runtime.InteropServices;
using HWND = System.IntPtr;
using System.Text;

/// <summary>Contains functionality to get all the open windows.</summary>
public static class OpenWindowGetter
{
    private const int BN_CLICKED = 245;
    /// <summary>Returns a dictionary that contains the handle and title of all the open windows.</summary>
    /// <returns>A dictionary that contains the handle and title of all the open windows.</returns>
    public static IDictionary<HWND, string> GetOpenWindows()
    {

        HWND shellWindow = GetShellWindow();
        Dictionary<HWND, string> windows = new Dictionary<HWND, string>();

        EnumWindows(delegate (HWND hWnd, int lParam)
        {

            if (hWnd == shellWindow) return true;
            if (!IsWindowVisible(hWnd)) return true;

            int length = GetWindowTextLength(hWnd);
            if (length == 0) return true;

            StringBuilder builder = new StringBuilder(length);
            GetWindowText(hWnd, builder, length + 1);


            if (builder.ToString() == "Export Selection") //Check for the export selection window
            {
                //Press the "save" button through code here
                IntPtr hwndChild = FindWindowEx((IntPtr)hWnd, IntPtr.Zero, "Button", "&Save");
                SendMessage((HWND)(int)hwndChild, BN_CLICKED, (HWND)0, IntPtr.Zero);
            }


            windows[hWnd] = builder.ToString();
            return true;

        }, 0);

        return windows;
    }

    private delegate bool EnumWindowsProc(HWND hWnd, int lParam);


    [DllImport("USER32.DLL")]
    private static extern int SetWindowText(HWND hWnd, String lpString);

    [DllImport("USER32.DLL")]
    private static extern bool EnumWindows(EnumWindowsProc enumFunc, int lParam);

    [DllImport("USER32.DLL")]
    private static extern int GetWindowText(HWND hWnd, StringBuilder lpString, int nMaxCount);

    [DllImport("USER32.DLL")]
    private static extern int GetWindowTextLength(HWND hWnd);

    [DllImport("USER32.DLL")]
    private static extern bool IsWindowVisible(HWND hWnd);

    [DllImport("USER32.DLL")]
    private static extern IntPtr GetShellWindow();

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string lclassName, string windowTitle);

    [DllImport("User32.dll")]
    public static extern Int32 SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
}



namespace ConsoleApp1
{
    class Program
    {
       
        static void Main(string[] args)
        {
            while (true)
            {


                foreach (KeyValuePair<IntPtr, string> window in OpenWindowGetter.GetOpenWindows())
                {
                    IntPtr handle = window.Key;
                    string title = window.Value;

                    //Console.WriteLine("{0}: {1}", handle, title);
                }

            }
        }
    }
}


I've been using FindWindowEx and SendMessage for controlling the window handles. This worked perfectly for the Save button, but now I'm trying to access the toolbar part of the window and set its text to the desired directory. I'm not even sure if this approach will work, but it seems the most simple. I've attached a screenshot for reference (the area circled in red is the handle I'm trying to access and change to a given file path before I "press" save)

Image

As seen in the photo I've been using Spy++ to get info about the Window and its handles. If I attempt this for example to get a pointer to that handle:

IntPtr hwndChildToolbar= FindWindowEx((IntPtr)hWnd, IntPtr.Zero, "ToolbarWindow32", "Address: "+"C:\\Windows\\System32");

It doesn't work. The "Caption" value seen in the Spy++ app changes to whatever the current directory name is, so trying to access that doesn't seem to make sense.

This next line of code does give me a pointer in return, but it's not the correct address. Worth noting is there are multiple handles on this window that fall under the ToolbarWindow32 class:

IntPtr hwndChildToolbar= FindWindowEx((IntPtr)hWnd, IntPtr.Zero, "ToolbarWindow32", null);

If I can find a way to get the correct handle, then from here I just want to use SetWindowText if that's possible and set its value to a string that's my intended file path.

To sum it up I need some way to easily set the directory, and I'm not sure if this way is possible. My C# knowledge is limited so anything helps!

1

There are 1 answers

0
Simon Mourier On

Here is a sample .NET Framework Console App that uses UI Automation, opens Notepad, type something and saves it as a file in the temp folder, using the Address bar on top of the Common Dialog.

See here .NET UI Automation Overview or here Native UI Automation for introduction and reference on UI Automation. It's usually better to use UI Automation than hacking windows handles. If you can't do it with UI Automation, there are chances you won't be able to do it with handles anyway. To discover the elements you can use and code against, you can use the Inspect tool from the Windows SDK.

Note I'm using here an interop version of the native UI Automation, as the .NET original wrappers that are provided with Windows have not been updated by Microsoft for many years for some reason.

// this code needs the "Interop.UIAutomationClient" Nuget package and "using Interop.UIAutomationClient"
class Program
{
    private static readonly CUIAutomation8 _automation = new CUIAutomation8();

    static void Main()
    {
        // track window open event
        var processId = 0;
        _automation.AddAutomationEventHandler(UIA_EventIds.UIA_Window_WindowOpenedEventId, _automation.GetRootElement(), TreeScope.TreeScope_Subtree, null,
            new AutomationEventHandler((window, id) =>
        {
            // check the process id we opened
            if (window.CurrentProcessId != processId)
                return;

            // get editor control
            var editor = window.FindFirst(TreeScope.TreeScope_Children, _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId));
            if (editor == null) // not the window we're looking for
                return;

            // get editor's value pattern & set some text value
            var value = (IUIAutomationValuePattern)editor.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId);
            value.SetValue("hello world");

            // get menu bar
            var menuBar = window.FindFirst(TreeScope.TreeScope_Children, _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_MenuBarControlTypeId));
            if (menuBar == null)
            {
                Console.WriteLine("Can't find menu bar.");
                return;
            }

            // get "File" menu item (beware of localization) & invoke (open)
            var file = menuBar.FindFirst(TreeScope.TreeScope_Children, _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, "File"));
            if (file == null)
            {
                Console.WriteLine("Can't find 'File' menu item.");
                return;
            }

            // expand "File" menu
            var expand = (IUIAutomationExpandCollapsePattern)file.GetCurrentPattern(UIA_PatternIds.UIA_ExpandCollapsePatternId);
            expand.Expand();

            do
            {
                // get the "Save" item by name from the window subtree (as the menu that opens is a child of the window)
                // do some retry to handle menu opening time
                var save = window.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreatePropertyConditionEx(UIA_PropertyIds.UIA_NamePropertyId, "Save", PropertyConditionFlags.PropertyConditionFlags_MatchSubstring));
                if (save != null)
                {
                    ((IUIAutomationInvokePattern)save.GetCurrentPattern(UIA_PatternIds.UIA_InvokePatternId)).Invoke();
                    break;
                }

            }
            while (true);

            // get the "Save As" dialog
            // do some retry to handle dialog opening time
            IUIAutomationElement dialog;
            do
            {
                dialog = window.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_LocalizedControlTypePropertyId, "dialog"));
                if (dialog != null)
                    break;
            }
            while (true);

            // get the "Previous locations" to enable the Address edit box
            var previous = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition(
                _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_ButtonControlTypeId),
                _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, "Previous Locations")));
            if (previous == null)
            {
                Console.WriteLine("Can't find 'Previous Locations' button.");
                return;
            }

            // push "Previous Locations" button
            var previousButton = (IUIAutomationInvokePattern)previous.GetCurrentPattern(UIA_PatternIds.UIA_InvokePatternId);
            previousButton.Invoke();

            // enter the directory path
            var address = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition(
                _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId),
                _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, "Address")));
            if (address == null)
            {
                Console.WriteLine("Can't find 'Address' edit.");
                return;
            }

            // sets the directory (here we use the temp directory)
            var edit = (IUIAutomationValuePattern)address.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId);
            edit.SetValue(System.IO.Path.GetTempPath());

            // push "Previous Locations" button again to "commit"
            previousButton.Invoke();

            // get the "File name:" edit
            // do some retry to handle folder refresh
            do
            {
                var fileName = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition(
                    _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_EditControlTypeId),
                    _automation.CreatePropertyConditionEx(UIA_PropertyIds.UIA_NamePropertyId, "File name", PropertyConditionFlags.PropertyConditionFlags_MatchSubstring)));
                if (fileName != null)
                {
                    // sets the file name (some "random" name)
                    ((IUIAutomationValuePattern)fileName.GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)).SetValue(@"hello" + Environment.TickCount + ".txt");
                    break;
                }
            }
            while (true);

            // get the "Save" button
            var dialogSave = dialog.FindFirst(TreeScope.TreeScope_Subtree, _automation.CreateAndCondition(
                _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_ControlTypePropertyId, UIA_ControlTypeIds.UIA_ButtonControlTypeId),
                _automation.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, "Save")));
            if (dialogSave == null)
            {
                Console.WriteLine("Can't find 'Save' button.");
                return;
            }

            // press the 'Save' button
            ((IUIAutomationInvokePattern)dialogSave.GetCurrentPattern(UIA_PatternIds.UIA_InvokePatternId)).Invoke();
        }));

        // start notepad
        var process = Process.Start("notepad");
        processId = process.Id;

        Console.WriteLine("Press any key to quit...");
        Console.ReadKey(false);
        try
        {
            process.CloseMainWindow();
        }
        catch
        {
            // maybe closed by something else, do nothing
        }
    }

    // helper class
    class AutomationEventHandler : IUIAutomationEventHandler
    {
        public AutomationEventHandler(Action<IUIAutomationElement, int> action)
        {
            if (action == null)
                throw new ArgumentNullException(nameof(action));

            Action = action;
        }

        public Action<IUIAutomationElement, int> Action { get; }
        public void HandleAutomationEvent(IUIAutomationElement sender, int eventId) => Action(sender, eventId);
    }
}