Search code examples
c#.netwinformswinapiopenfiledialog

How to retrieve the last folder used by OpenFileDialog?


In my program I use an OpenFileDialog component and I set an initial directory.

However, if I decide to open a file from another folder, next time I want to open a file, the OpenFileDialog remembers what was last folder I used to open a file and it will open that folder, not the folder specified in InitialDirectory. And I am happy it is this way. Even if I close and reopen the application, the last folder used will still be remembered.

Is there a way to find out which is folder? Let's say, when I click on a Button, I want the last path displayed in a Label.

OpenFileDialog ofd = new OpenFileDialog();
ofd.InitialDirectory = @"C:\My Initial Folder";

Solution

  • Disclaimer:
    the procedure described here is not supported: anything can change at any time without notice.


    ▶ The last path accessed by an application when the OpenFileDialog or SaveFileDialog are used to open or save a file, is stored in the Registry, in the HKEY_CURRENT_USER branch, under this Key:

    Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedPidlMRU
    

    ▶ The names of the more recent files opened or saved by an application, are stored - organized by file extension - under this other Key (same Registry branch):

    Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSavePidlMRU
    

    All the information is stored as a Binary Value.
    The first Key - LastVisitedPidlMRU - stores the last path accessed by an application, in two possible flavors:

    • A Parent shell folder item, in the classic GUID form (usually representing the Desktop folder), followed by the child IDlist where the path is stored in both Unicode long path format and DOS short path format (and other small pieces of information not described here - see the Further reading links).
    • A null-terminated Unicode string, representing the Executable name, followed by an IDlist (same as before).
      Here, I'll be using this structure to retrieve the last path used by a specified executable.

    The Binary Values contained in the Sub-Key items of the OpenSavePidlMRU Key are stored as a simple IDlist, so the stored Paths can be accessed directly using both SHGetPathFromIDListW() and SHGetPathFromIDListEx().

    Here, I'm declaring both (may come in handy), but I'm using only the second, since it's slightly more flexible (well, it allows to retrieve the short path form, if required)

    Further reading or testing:


    There are three methods here:

    ApplicationGetLastOpenSavePath is used to retrieve the last path used by an application, given its Executable name (just the name, e.g., app.exe).
    Since this question is tagged WinForms, you can get it as:

    string exeName = Path.GetFileName(Application.ExecutablePath);
    

    ApplicationGetLastOpenSaveFileNames retrieves all file names opened by an application, in relation to the last accessed path (and this alone, since this is what the current question is about).
    The method returns a named tuple, (string AppPath, List<string> FileNames):

    • AppPath is the last accessed path.
    • FileNames is a List of file names - where the path is AppPath - accessed by the application.

    → The third is a helper method: its task is to handle the call to SHGetPathFromIDListEx(), which requires some interop marshaling.
    The method parameters represent the byte array extracted from the Registry Key and an offset, which represents the initial location of the IDlist (as described, the binary data starts with the null-terminated Unicode string representing the executable name).

    private string ApplicationGetLastOpenSavePath(string executableName)
    {
        if (string.IsNullOrEmpty(executableName)) return null;
        string lastVisitedPath = string.Empty;
        var lastVisitedKey = Registry.CurrentUser.OpenSubKey(
            @"Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedPidlMRU", false);
    
        string[] values = lastVisitedKey.GetValueNames();
        foreach (string value in values) {
            if (value == "MRUListEx") continue;
            var keyValue = (byte[])lastVisitedKey.GetValue(value);
    
            string appName = Encoding.Unicode.GetString(keyValue, 0, executableName.Length * 2);
            if (!appName.Equals(executableName)) continue;
    
            int offset = executableName.Length * 2 + "\0\0".Length;  // clearly null terminated :)
            lastVisitedPath = GetPathFromIDList(keyValue, offset);
            break;
        }
        return lastVisitedPath;
    }
    

    This second method calls the first to retrieve the last path accessed by the application represented by string executableName:

    private (string AppPath, List<string> FileNames) ApplicationGetLastOpenSaveFileNames(string executableName)
    {
        string appPath = ApplicationGetLastOpenSavePath(executableName);
        if (string.IsNullOrEmpty(appPath)) return (null, null);
    
        var fileNames = new List<string>();
        var extensionMRUKey = Registry.CurrentUser.OpenSubKey(
            @"Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSavePidlMRU", false);
        string[] subKeys = extensionMRUKey.GetSubKeyNames().ToArray();
    
        foreach (string key in subKeys) {
            var subKey = extensionMRUKey.OpenSubKey(key);
    
            foreach (string keyValue in subKey.GetValueNames()) {
                var value = (byte[])subKey.GetValue(keyValue);
                string filePath = GetPathFromIDList(value, 0);
    
                if (filePath.Contains(appPath)) {
                    fileNames.Add(filePath);
                }
            }
        }
        return (appPath, fileNames);
    }
    

    Helper method and Win32 declarations:

    private string GetPathFromIDList(byte[] idList, int offset)
    {
        int buffer = 520;  // 520 = MAXPATH * 2
        var sb = new StringBuilder(buffer);
    
        IntPtr ptr = Marshal.AllocHGlobal(idList.Length);
        Marshal.Copy(idList, offset, ptr, idList.Length - offset);
    
        // or -> bool result = SHGetPathFromIDListW(ptr, sb);
        bool result = SHGetPathFromIDListEx(ptr, sb, buffer, GPFIDL_FLAGS.GPFIDL_UNCPRINTER);
        Marshal.FreeHGlobal(ptr);
        return result ? sb.ToString() : string.Empty;
    }
    
    [DllImport("shell32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
    internal static extern bool SHGetPathFromIDListW(
        IntPtr pidl, 
        [MarshalAs(UnmanagedType.LPTStr)]
        StringBuilder pszPath);
    
    [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
    internal static extern bool SHGetPathFromIDListEx(
        IntPtr pidl, 
        [MarshalAs(UnmanagedType.LPTStr)]
        [In,Out] StringBuilder pszPath, 
        int cchPath, 
        GPFIDL_FLAGS uOpts);
    
    internal enum GPFIDL_FLAGS : uint {
        GPFIDL_DEFAULT = 0x0000,
        GPFIDL_ALTNAME = 0x0001,
        GPFIDL_UNCPRINTER = 0x0002
    }