potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# シェルの「shell:~」形式からパスを取得する

Windowsではシェルに「shell:~」形式の名前を与えると特殊フォルダを開けます。C#でこの形式からパスを取得するには、一度Win32 APIParseDisplayName関数でアイテムIDリストを取得して、取得したアイテムIDリストをWin32 APISHGetPathFromIDListEx関数等に与えます。SHGetPathFromIDListEx関数は文字列バッファーの長さを受け取りますが、長さが元のパスの長さに満たなくても成功を返すことに注意が必要です。

クラス 概要
Program Main関数です。
ShellIDListUtility シェルのアイテムIDリストのユーティリティ関数を提供するクラスです。Win32 APIのラッパーです。
SafeCoTaskMemPointer アンマネージドリソースのMarshal.FreeCoTaskMem関数による解放を保証するためのクラスです。SafeHandleの派生クラスです。
using System;
using System.Runtime.InteropServices;
using System.Text;

class Program
{
    static void Main()
    {
        using var pidl = ShellIDListUtility.ParseDisplayName("shell:common startup");
        var path = ShellIDListUtility.GetPath(pidl);
    }
}

internal static class ShellIDListUtility
{
    public static SafeCoTaskMemPointer ParseDisplayName(string name)
    {
        NativeMethods.SHParseDisplayName(name, default, out var pidl, 0, out var _);
        return pidl;
    }

    public static string GetPath(SafeHandle pidl)
    {
        const int EXPANDING = 260;
        var builder = new StringBuilder();
        for (int i = EXPANDING; i < int.MaxValue; i += EXPANDING)
        {
            builder.Capacity = i;
            NativeMethods.SHGetPathFromIDListEx(
                pidl, builder, (uint)builder.Capacity, 0);
            if (builder.Length != i - 1)
            {
                return builder.ToString();
            }
        }
        return null;
    }

    private static class NativeMethods
    {
        [DllImport("shell32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
        public static extern int SHParseDisplayName(
            [In] string pszName,
            IntPtr pbc,
            out SafeCoTaskMemPointer ppidl,
            uint sfgaoIn,
            out uint psfgaoOut);

        [DllImport("shell32.dll", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SHGetPathFromIDListEx(
            SafeHandle pidl,
            [Out] StringBuilder pszPath,
            uint cchPath,
            uint uOpts);
    }
}

internal sealed class SafeCoTaskMemPointer : SafeHandle
{
    private SafeCoTaskMemPointer()
        : base(default, true)
    {
    }

    public SafeCoTaskMemPointer(IntPtr handle, bool ownsHandle)
        : base(handle, ownsHandle)
    {
    }

    public override bool IsInvalid => handle == default;

    protected override bool ReleaseHandle()
    {
        Marshal.FreeCoTaskMem(handle);
        return true;
    }
}