potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。

C#9&Win API ドロップされたオブジェクトの表示名を取得する。

Windows APIを使用してごみ箱やPCのような特殊オブジェクトの表示名を取得するコードです。SHCreateShellItemArrayFromDataObject関数を使用しています。

#nullable enable

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;

using ComTypes = System.Runtime.InteropServices.ComTypes;

namespace WinFormsApp1;

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.Run(new Form1());
    }
}

sealed class Form1 : Form
{
    private TextBox textBox1;

    public Form1()
    {
        AllowDrop = true;
        DragEnter += Form1_DragEnter;
        DragDrop += Form1_DragDrop;

        textBox1 = new()
        {
            Dock = DockStyle.Fill,
            Multiline = true,
            ReadOnly = true,
            WordWrap = false,
            ScrollBars = ScrollBars.Both
        };
        Controls.AddRange(new[] { textBox1 });
    }

    private const string CFSTR_SHELLIDLIST = "Shell IDList Array";

    private void Form1_DragEnter(object? sender, DragEventArgs e)
    {
        if (e.Data is null || !e.Data.GetDataPresent(CFSTR_SHELLIDLIST))
            return;
        e.Effect = DragDropEffects.Move;
    }

    private void Form1_DragDrop(object? sender, DragEventArgs e)
    {
        if (e.Data is null || !e.Data.GetDataPresent(CFSTR_SHELLIDLIST))
            return;

        var items = ShellItemUtility.CreateShellItemArrayForDataObject(e.Data);

        textBox1.Text = string.Join<string>(
            Environment.NewLine,
            items.Select<string>(item => item.GetDisplayName()));

        Marshal.FinalReleaseComObject(items);
    }
}

/// <summary>
/// IShellItem関係のユーティリティ
/// </summary>
static class ShellItemUtility
{
    /// <summary>
    /// DataObjectからIShellItemArrayを作成します。
    /// </summary>
    public static IShellItemArray CreateShellItemArrayForDataObject(IDataObject dataObj)
    {
        var hr = NativeMethods.SHCreateShellItemArrayFromDataObject(
            (ComTypes.IDataObject)dataObj,
            typeof(IShellItemArray).GUID,
            out var unkItems);
        if (hr != 0 || unkItems as IShellItemArray is not { } items)
            throw Marshal.GetExceptionForHR(hr)!;
        return items;
    }

    /// <summary>
    /// IShellItemArrayの各IShellItemに処理を適用します。
    /// </summary>
    public static void ForEach(this IShellItemArray items, Action<IShellItem> action)
    {
        var hr = items.GetCount(out var itemCount);
        if (hr != 0) Marshal.ThrowExceptionForHR(hr);

        for (uint i = 0; i < itemCount; i++)
        {
            hr = items.GetItemAt(i, out var item);
            try
            {
                if (hr != 0) Marshal.ThrowExceptionForHR(hr);
                action(item);

            }
            finally
            {
                Marshal.FinalReleaseComObject(item);
            }
        }
    }

    /// <summary>
    /// IShellItemArrayの各IShellItemに処理を適用した結果を返します。
    /// </summary>
    public static IEnumerable<T> Select<T>(this IShellItemArray items, Converter<IShellItem, T> converter)
    {
        var hr = items.GetCount(out var itemCount);
        if (hr != 0) Marshal.ThrowExceptionForHR(hr);

        for (uint i = 0; i < itemCount; i++)
        {
            hr = items.GetItemAt(i, out var item);
            try
            {
                if (hr != 0) Marshal.ThrowExceptionForHR(hr);
                yield return converter(item);

            }
            finally
            {
                Marshal.FinalReleaseComObject(item);
            }
        }
    }

    /// <summary>
    /// IShellItemの表示名を取得します。
    /// </summary>
    public static string GetDisplayName(this IShellItem item)
    {
        var hr = item.GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY, out var pname);
        using (pname)
        {
            if (hr != 0) throw Marshal.GetExceptionForHR(hr)!;
            return pname.ToString();
        }
    }

    private static class NativeMethods
    {
        [DllImport("shell32.dll")]
        public static extern int SHCreateShellItemArrayFromDataObject(
            ComTypes.IDataObject pdo,
            in Guid riid,
            [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
    }
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b63ea76d-1f85-456f-a19c-48159efa858b")]
interface IShellItemArray
{
    [PreserveSig]
    int BindToHandler(
        [MarshalAs(UnmanagedType.IUnknown)] object pbc, // IBindCtx
        in Guid bhid,
        in Guid riid,
        [MarshalAs(UnmanagedType.IUnknown)] out object ppvOut);

    [PreserveSig]
    int GetPropertyStore(
        uint flags, // GETPROPERTYSTOREFLAGS
        in Guid riid,
        [MarshalAs(UnmanagedType.IUnknown)] out object ppv);

    [PreserveSig]
    int GetPropertyDescriptionList(
        in Guid keyType, // REFPROPERTYKEY
        in Guid riid,
        [MarshalAs(UnmanagedType.IUnknown)] out object ppv);

    [PreserveSig]
    int GetAttributes(
        uint AttribFlags, // SIATTRIBFLAGS
        uint sfgaoMask, // SFGAOF
        out uint psfgaoAttribs); // SFGAOF 

    [PreserveSig]
    int GetCount(
        out uint pdwNumItems);

    [PreserveSig]
    int GetItemAt(
        uint dwIndex,
        out IShellItem ppsi);

    [PreserveSig]
    int EnumItems(
        [MarshalAs(UnmanagedType.IUnknown)] out object ppenumShellItems); // IEnumShellItems
}

enum SIGDN : uint
{
    SIGDN_NORMALDISPLAY = 0,
    SIGDN_PARENTRELATIVEPARSING = 0x80018001,
    SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
    SIGDN_PARENTRELATIVEEDITING = 0x80031001,
    SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
    SIGDN_FILESYSPATH = 0x80058000,
    SIGDN_URL = 0x80068000,
    SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
    SIGDN_PARENTRELATIVE = 0x80080001,
    SIGDN_PARENTRELATIVEFORUI = 0x80094001
}

enum SICHINTF : uint
{
    SICHINT_DISPLAY = 0,
    SICHINT_ALLFIELDS = 0x80000000,
    SICHINT_CANONICAL = 0x10000000,
    SICHINT_TEST_FILESYSPATH_IF_NOT_EQUAL = 0x20000000
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
interface IShellItem
{
    [PreserveSig]
    int BindToHandler(
        [MarshalAs(UnmanagedType.IUnknown)] object pbc, // IBindCtx
        in Guid bhid,
        in Guid riid,
        [MarshalAs(UnmanagedType.IUnknown)] out object ppv);

    [PreserveSig]
    int GetParent(
        out IShellItem ppsi);

    [PreserveSig]
    int GetDisplayName(
        SIGDN sigdnName,
        out SafeCoTaskMemString ppszName);

    [PreserveSig]
    int GetAttributes(
        uint sfgaoMask, // SFGAOF
        out uint psfgaoAttribs); // SFGAOF

    [PreserveSig]
    int Compare(
        [In] IShellItem psi,
        SICHINTF hint,
        out int piOrder);
}

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

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

    public override bool IsInvalid
        => handle == default;

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

    public override string ToString()
        => Marshal.PtrToStringUni(handle) ?? "";
}