potisanのプログラミングメモ

プログラミング素人です。昔の自分を育ててくれたネット情報に少しでも貢献できるよう、情報を貯めていこうと思っています。Windows環境のC++やC#がメインです。

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 readonly 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(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);
        if (hr != 0) throw Marshal.GetExceptionForHR(hr)!;
        return pname!;
    }

    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,
        [MarshalAs(UnmanagedType.LPWStr)] out string? ppszName);

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

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