如何检索 OpenFileDialog 使用的最后一个文件夹?

How to retrieve the last folder used by OpenFileDialog?

在我的程序中,我使用了一个 OpenFileDialog 组件并设置了一个初始目录。

但是,如果我决定从另一个文件夹打开文件,下次我想打开文件时,OpenFileDialog 会记住我上次打开文件时使用的文件夹,它会打开那个文件夹,而不是文件夹在 InitialDirectory 中指定。我很高兴它是这样的。即使我关闭并重新打开应用程序,上次使用的文件夹仍会被记住。

有没有办法找出哪个是文件夹?比方说,当我单击一个按钮时,我希望在标签中显示最后一个路径。

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

在Windows中有一个"Working Directory"的概念。基本上是应用程序现在正在查看的一个文件夹。那个用于完成任何相对路径,如果您只放置一个不是关键字的随机字符串,则首先检查该文件夹是否有任何可执行文件。起始值由启动应用程序的命令定义 - 这将始终是完整路径。

没有 InitialDirectory,OpenFileDialog 显然会使用它。默认情况下,它不会设置回去:https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.filedialog.restoredirectory

如果您在第二次调用中省略 InitialDirectory,它将自动以这种方式工作。但是当然你也可以适当地提取工作目录值。一些简单的 bool 开关可以做到这一点,但是您必须在为对话框设置目录的任何地方复制它们:

bool InitializedDialogs = false;

if(InitializedDialogs == false){
   ofd.InitialDirectory = @"C:\My Initial Folder";
   InitializedDialogs = true;
}

在中心位置对 https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.setcurrentdirectory 进行一次调用可能会更好。

没有任何方法可以为您提供该信息,您只需要自己跟踪即可。

像这样:

private string lastFolder;

private void button1_Click(object sender, System.EventArgs e)
{
    if (ofd.ShowDialog() == DialogResult.OK)
    {
        lastFolder = Path.GetDirectoryName(ofd.Filename);
        // Do something with the file.
    }
}

当用户 select 一个文件时,我会自动强制 InitialDirectory 成为该文件名的目录。每次我再次打开 OpenFileDialog 时都会转到最后一个文件夹。也许对你有帮助。请记住,我在另一个地方启动了 InitiaDirectory,而不是在我打开 OpenFileDialog 的情况下。

ofd.InitialDirectory = Path.GetDirectoryName(ofd.FileName);

免责声明:
不支持此处描述的过程:任何内容都可能随时更改,恕不另行通知。


▶ 当 OpenFileDialog 或 SaveFileDialog 用于打开或保存文件时,应用程序访问的最后一个路径存储在注册表中的 HKEY_CURRENT_USER 分支中,在该键下:

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

▶ 应用程序打开或保存的较新文件的名称存储在 - 按文件扩展名组织 - 在另一个密钥(相同的注册表分支)下:

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

所有信息都存储为二进制值。
第一个键 - LastVisitedPidlMRU - 存储应用程序访问的最后一个路径,有两种可能的形式:

  • 一个父 shell 文件夹项目,采用经典的 GUID 形式(通常代表桌面文件夹),后面是子 IDlist,其中路径以 Unicode 长路径格式和 DOS 格式存储短路径格式(以及此处未描述的其他小信息 - 请参阅 进一步阅读 链接)。
  • 一个以 null 结尾的 Unicode 字符串,表示可执行文件名称,后跟一个 IDlist(与之前相同)。
    在这里,我将使用此结构来检索指定可执行文件使用的最后一个路径。

OpenSavePidlMRU键的子键项中包含的二进制值存储为简单的IDlist,因此存储的路径可以是使用 SHGetPathFromIDListW() and SHGetPathFromIDListEx().

直接访问

在这里,我声明了两者(可能会派上用场),但我只使用了第二个,因为它稍微灵活一些(好吧,如果需要,它允许检索短路径形式)

进一步阅读或测试:


这里有三种方法:

ApplicationGetLastOpenSavePath 用于检索应用程序使用的最后一个路径,给定其可执行文件名称(只是名称,例如 app.exe) .
由于这个问题被标记为 WinForms,您可以将其作为:

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

ApplicationGetLastOpenSaveFileNames 检索应用程序打开的所有文件名,与上次访问的路径相关(仅此一项,因为这就是当前的问题大约)。
方法returns一个命名元组,(string AppPath, List<string> FileNames):

  • AppPath是最后访问的路径。
  • FileNames 是文件名列表 - 其中路径为 AppPath - 由申请。

→ 第三个是辅助方法:它的任务是处理对 SHGetPathFromIDListEx() 的调用,这需要一些互操作编组。
方法参数表示从注册表项和偏移量中提取的字节数组,偏移量表示 IDlist 的初始位置(如上所述,二进制数据以表示可执行文件名称的空终止 Unicode 字符串开头)。

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 + "[=13=][=13=]".Length;  // clearly null terminated :)
        lastVisitedPath = GetPathFromIDList(keyValue, offset);
        break;
    }
    return lastVisitedPath;
}

第二个方法调用第一个方法来检索由 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);
}

辅助方法和 Win32 声明:

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
}