在.NET中获取所有“特殊文件夹”

目录

介绍

背景

错误的方法 #1:将文件夹名称附加到用户主目录

错误方法#2:使用“Shell Folders”注册表项

正确的解决方案

了解本机方法

在C#中调用本机方法

把它们放在一起

使用代码

兴趣点

NuGet包


介绍

Windows 98开始,用户的主目录中有特殊文件夹,称为我的文档我的音乐我的图片我的视频,正确地称为已知文件夹。这些在XP之前几乎没有变化,并且它们的路径很容易用.NET检索:调用System.Environment.GetFolderPath函数并传入System.Environment.SpecialFolderenum值。

但是,自Windows Vista以来引入的较新文件夹未在SpecialFolder enum中列出,并且无法以这种方式检索。.NET未更新以反映对用户主目录(以及其他几个特殊文件夹)的添加,并且人们(包括我)在查找其他文件夹的路径时进行了粗俗和错误的尝试。本文详细介绍了这些尝试不正确的原因,并最终介绍了获取新的特殊文件夹路径的正确方法。

背景

我通常会看到这两次错误的尝试来检索——比如说——下载文件夹的路径。

错误的方法 #1:将文件夹名称附加到用户主目录

检索下载文件夹路径的看似最简单的方法是将特殊文件夹名称附加到用户主路径(默认情况下会在其中找到特殊文件夹):

// This returns something like C:\Users\Username:
string userRoot = System.Environment.GetEnvironmentVariable("USERPROFILE");
// Now let's get C:\Users\Username\Downloads:
string downloadFolder = Path.Combine(userRoot, "Downloads");

由于Windows Vista和更新版本在内部使用英文文件夹名称,并且文件资源管理器中显示的名称只是它们的虚拟本地化版本,这应该可以工作,不是吗?

不是的。如果用户重定向了下载文件夹的路径,则此解决方案不起作用。可以更改下载文件夹属性中的路径,只需为其选择另一个位置。它可能根本不在用户根目录中,它可以在任何地方——例如,我的都在D:分区上。

错误方法#2:使用“Shell Folders”注册表项

在下一次尝试中,开发人员在注册表中搜索包含重定向文件夹路径的键。Windows必须将这些信息存储在某个地方,对吗?

最终,他们找到了HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders。该键看起来非常有前途,因为它包含用户文件夹的所有路径,即使使用重定向路径(我的都在D:分区上,如您所见):

但是等等,它还包含一个名为!不要使用此注册表项和值使用SHGetFolderPathSHGetKnownFolderPath函数代替的不祥键。这个键是微软开发人员Raymond Chen放在那里的,他有正确的感觉,人们将来会继续滥用这个键来获取文件夹路径。他在这里这里写了关于它的文章。

总而言之,使用此键仅在Windows 95 beta版本中是可接受的,而且不久之后。微软注意到它不够灵活,无法保留有关外壳文件夹的所有信息,它不尊重漫游用户配置文件等等。而是创建了一个WinAPI函数,但是键临时保留在注册表中,以免破坏RTMWin95测试阶段设计的四个程序。后来再也没有被删除,因为更多的开发人员发现了这个键并开始依赖它,现在删除它会使更多的程序不兼容,而不是原来的四个。

所以,不要使用那里的值。它们不能保证是正确的,甚至不能保证存在,Raymond Chen可能会因为这样做而讨厌你——你最好不要惹上微软的Chuck Norris的麻烦。甚至我曾经遇到过“Shell Folders”关键陷阱,并将其作为解决方案发布在StackOverflow答案中(我同时更新了该答案)。

正确的解决方案

作为优秀的开发人员,我们遵循key的建议,以及P/Invoke所说的SHGetKnownFolderPath函数。由于P/Invoke可能有点讨厌,让我们一起解决它。

首先,我们定义最终要如何检索路径。最简单的解决方案应该是一个接受我们自己的参数的static方法,扩展的“特殊”文件夹enum:

string downloadsFolder = KnownFolders.GetPath(KnownFolder.Downloads);

enum KnownFolder
{
    Documents,
    Downloads,
    Music,
    Pictures,
    SavedGames,
    // ...
}

static class KnownFolders
{
    public static string GetPath(KnownFolder folder)
    {
        // TODO: Implement
    }
}

了解本机方法

接下来,我们必须了解Microsoft文档中指定的SHGetKnownFolderPath方法。我在下面引用了最重要的部分:

HRESULT SHGetKnownFolderPath(
  [in]           REFKNOWNFOLDERID rfid,
  [in]           DWORD            dwFlags,
  [in, optional] HANDLE           hToken,
  [out]          PWSTR            *ppszPath
);

  • [in] REFKNOWNFOLDERID rfid对标识文件夹的KNOWNFOLDERID的引用。

KNOWNFOLDERID实际上是一个GUID可在此处找到可用的GUID 。我们必须将它们映射到我们的KnownFolder枚举值。为简单起见,我们将只在我们的static类中使用其中的一些和字典。

  • [in] DWORD dwFlags指定特殊检索选项的标志。该值可以是 0;否则,一个或多个KNOWN_FOLDER_FLAG值。

为简单起见,我们确实会使用0,尽管您可以调整和优化行为以满足您的期望。如果您不需要确保文件夹是否已经创建(如果它还不存在),那么KF_FLAG_DONT_VERIFY可能会很有用,如果文件夹被重新定位到网络驱动器,这个操作可能会很慢。

  • [in, optional] HANDLE hToken"如果此参数为NULL,这是最常见的用法,该函数为当前用户请求已知文件夹。"

为简单起见,我们只关心执行用户的文件夹路径。您可以传入System.Security.Principal.WindowsIdentity.AccessToken的句柄来模拟另一个用户。

  • [out] PWSTR *ppszPath当此方法返回时,包含指向以空字符结尾的Unicode字符串的指针的地址,该字符串指定已知文件夹的路径。调用进程负责在不再需要此资源时通过调用CoTaskMemFree来释放此资源,无论SHGetKnownFolderPath成功与否。

这最终将返回我们感兴趣的路径。

C#中调用本机方法

要在C#中调用此方法,可以使用以下导入。下面的列表解释了这是如何确定的。如果您对P/Invoke在后台的工作方式不感兴趣,可以跳过它。

[DllImport("shell32", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath(
    [MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, nint hToken = default);

  • 文档说明该方法驻留在模块Shell32.dll中,因此我们将此文件名提供给DllImport属性(您不必指定文件扩展名)。
  • 由于作为[out]参数返回给我们的路径是一个unicode(UTF16)字符串,我们确保覆盖C#的默认CharSet值为CharSet.Unicode。这允许我们直接转换 PWSTR *ppszPathstring,编组器释放为其分配的本机内存——请注意,这仅适用于编组器假设此类内存块先前已分配CoTaskMemAlloc(这是WinAPI方法所做的),总是为我们调用CoTaskMemFree它们。
  • 我们提供ExactSpelling = true,因为没有AW版本的方法,并防止运行时搜索这样的。
  • 使用PreserveSig = false 允许我们将方法返回的任何 HRESULT失败代码转换为.NET异常。它也使方法返回void,但我们可以进一步改变它以返回最后一个[out]参数,在这种情况下,是我们的string
  • 我们可以映射 REFKNOWNFOLDERID GUID ref Guid,但不必处理引用,当我们提供Guid按值[MarshalAs(UnmanagedType.LPStruct)]时,我们可以指示编组器为我们执行此操作。
  • 下一个参数是 DWORD,在这种情况下,它可以映射到由可用标志组成的.NET enum,但由于我们对它们不感兴趣,我们只 相应地使用原始的 uint
  • HANDLE是本机整数的大小,因此我们为此使用C# 9的新的nint——或者,您仍然可以使用IntPtr。该参数是可选的,即使我们不需要这样做,我们也会用= default

把它们放在一起

填写我们static类的实现,我们将得到以下内容:

using System.Runtime.InteropServices;

static class KnownFolders
{
    private static readonly Dictionary<KnownFolder, Guid> _knownFolderGuids = new()
    {
        [KnownFolder.Documents] = new("FDD39AD0-238F-46AF-ADB4-6C85480369C7"),
        [KnownFolder.Downloads] = new("374DE290-123F-4565-9164-39C4925E467B"),
        [KnownFolder.Music] = new("4BD8D571-6D19-48D3-BE97-422220080E43"),
        [KnownFolder.Pictures] = new("33E28130-4E1E-4676-835A-98395C3BC3BB"),
        [KnownFolder.SavedGames] = new("4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4"),
    };

    public static string GetPath(KnownFolder folder)
    {
        return SHGetKnownFolderPath(_knownFolderGuids[folder], 0);
    }

    [DllImport("shell32", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
    private static extern string SHGetKnownFolderPath(
        [MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, nint hToken = default);
}

使用代码

我们使打印通过我们enum暴露的所有已知文件夹路径变得非常简单:

foreach (KnownFolder knownFolder in Enum.GetValues<KnownFolder>())
{
    try
    {
        Console.Write($"{knownFolder}: ");
        Console.WriteLine(KnownFolders.GetPath(knownFolder));
    }
    catch (Exception ex)
    {
        Console.WriteLine($"<Exception> {ex.Message}");
    }
    Console.WriteLine();
}

由于P/InvokeHRESULT转换为广泛的异常,因此需要非常宽松的try catch。如果某个文件夹在系统上不可用,您可能会遇到FileNotFoundException,但这通常不会发生在用户文件夹中。

兴趣点

我们没有触及所有相关的已知文件夹功能,并提到了一些可能的优化潜力:

  • 将剩余的文件夹添加到我们的enum
  • 在自定义KnownFolderException中包装异常以更具体地捕获它们。
  • 使用属性而不是dictionaryGUID分配给每个KnownFolder enum值并通过反射检索它们。
  • 使用SHSetKnownFolderPath更改已知文件夹的路径。
  • 通过传入身份访问令牌句柄来查询另一个用户的路径。
  • 通过SHGetKnownFolderItem检索IShellItem COM对象实例,并使用GetDisplayName方法提取用户友好的文件夹名称。
  • 添加对没有该SHGetKnownFolderPath函数的Windows XP操作系统和更早版本的兼容性,创建一个能够为它们检索路径的包装器,可能只是回退到System.Environment.GetFolderPath()
  • 使用CsWin32WinAPI元数据自动生成P/Invoke签名。请注意,其签名使用原始 unsafe语义,与本文中确定的签名不同。

如果您喜欢这篇文章,请随时在StackOverflow上支持我的回答

NuGet

我创建了一个NuGet包,提供了上一段中讨论的大部分功能。有关它的更多信息,请访问其项目网站。请注意,由于附加功能,API略有不同,请查看README了解更多详细信息。

https://www.codeproject.com/Articles/878605/Getting-All-Special-Folders-in-NET

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值