【问题标题】:How to retrieve the last folder used by OpenFileDialog?如何检索 OpenFileDialog 使用的最后一个文件夹?
【发布时间】:2020-08-17 12:11:02
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: c# .net winforms winapi openfiledialog


    【解决方案1】:

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


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

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

    ▶ 应用程序打开或保存的最新文件的名称存储 - 按文件扩展名组织 - 在此其他键(相同的注册表分支)下:

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

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

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

    OpenSavePidlMRU键的子键项中包含的二进制值存储为简单的IDlist,因此可以使用SHGetPathFromIDListW()和@直接访问存储的路径987654322@.

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

    进一步阅读或测试:


    这里有三种方法:

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

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

    ApplicationGetLastOpenSaveFileNames 检索应用程序打开的所有文件名,与最后访问的路径相关(仅此一项,因为这是当前问题的内容)。
    该方法返回一个命名元组,(string AppPath, List<string> FileNames)

    • AppPath 是最后访问的路径。
    • FileNames 是文件名列表 - 其中路径为 AppPath - 由应用程序访问。

    → 第三个是辅助方法:它的任务是处理对 SHGetPathFromIDListEx() 的调用,这需要一些互操作编组。
    方法参数表示从 Registry Key 中提取的字节数组和一个偏移量,它表示 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 + "\0\0".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);
    }
    

    Helper 方法和 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
    }
    

    【讨论】:

      【解决方案2】:

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

      类似这样的:

      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.
          }
      }
      

      【讨论】:

      • 实际上,对话框会跟踪它。或者更确切地说,Windows 通过“工作目录”进行。一个古老的 DOS 时代的遗物,它仍然在使用中。
      • 是的,但没有“检索”该信息的方法。
      • 谢谢,但这并不总是有效。如果我关闭应用程序并重新打开它,我想知道该文件夹而不打开文件。也许这不是一个重要的功能,我想也许有一个我不知道的简单方法。
      • 我可以将该文件夹名称保存到一个文件中,而不是一个全局变量,这样可以工作(虽然不是很优雅)。
      • @jscarle “是的,但没有办法“检索”该信息。”现实不同意:docs.microsoft.com/en-us/dotnet/api/… |同样,这只是该死的工作目录。 早在几年前就已经是一件事了。
      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

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

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

        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 可能会更好。

        【讨论】:

        • 感谢您的回答,但它没有回答我的问题。我不想改变程序如何工作的行为,我只想事先找到,当我调用 OpenFileDialog 组件时会打开哪个目录。如果我使用 Directory.GetCurrentDirectory() 是可执行文件所在的目录,而不是 OpenFileDialog() 使用的最后一个文件夹。
        • @Nick_F 你明白如果你想要不同的结果,你不会围绕改变程序的功能吗? |现在目标也不同了。但是,解决方案是相同的:使用docs.microsoft.com/en-us/dotnet/api/… 获取要在关闭时保存的目录。启动时使用docs.microsoft.com/en-us/dotnet/api/…进行设置。在程序第一次启动时,初始化它之前用于InitialDirectory 的任何内容。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-22
        • 1970-01-01
        • 1970-01-01
        • 2013-09-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多