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) ?? "";
}

R&tidyverse purrr::map系関数とformulaの引数指定

purrr::map系関数はformula~...)で式を与えられます。引数は..x..1のどれでも指定できます。

require(purrr)

1:10 %>% map_dbl(~.+1)
1:10 %>% map_dbl(~.x+1)
1:10 %>% map_dbl(~..1+1)
#> [1]  2  3  4  5  6  7  8  9 10 11

purrr::map2系関数やpurrr::pmap系関数では引数が複数になるため、..x..1の他にも.y..2などが使われます。

参考

R&tidyverse purrr::map_intが単純な足し算で失敗する理由

purrr::map_int関数が単純な足し算で失敗したとき、数値リテラルの型が原因かもしれません。

次のコードで(1)はエラー、(2)は成功します。違いは足す数が11Lかです。

require(purrr)

1:10 %>% map_int(~.x+1)
#> Error: Can't coerce element 1 from a double to a integer
1:10 %>% map_int(~.x+1L)
#> [1]  2  3  4  5  6  7  8  9 10 11

原因はRの数値リテラルサフィックスなしでdouble型であることです。これは次のコードで確かめられます。

typeof(1)  # [1] "double"
typeof(1L) # [1] "integer"

purrr::map_int関数は戻り値をinteger型に制限しているので、~.+1の結果がdouble型となることからエラーとなります。解決策は上記コードのように1Lサフィックスで型指定するか、purrr::map_dbl関数を使うことです。

参考: - https://stackoverflow.com/questions/55397509/purrrmap-int-cant-coerce-element-1-from-a-double-to-a-integer - 3.1.1 Constants - R Language Definition - purrr::map - tidyverse

Power Query M テーブルの列名を先頭・末尾からの個数・条件一致で抜き出す

Power Query Mでテーブルの列名を先頭・末尾から個数あるいは条件一致で抽出するコードです。

let
    // 列名用のダミーテーブル
    テーブル1 = #table(
        {"列1", "列2", "列3", "列4", "列5", "A", "B", "C"},
        {}
    ),
    // 列名はTable.ColumnNames関数でリストとして取り出せます。
    // リストの通常操作で列名の一部を取り出せます。
    列名の一部 = [
        列名 = Table.ColumnNames(テーブル1),
        先頭3 = List.FirstN(列名, 3),
        末尾3 = List.LastN(列名, 3),
        先頭3以外 = List.RemoveFirstN(列名, 3),
        末尾3以外 = List.RemoveLastN(列名, 3),
        #"先頭から『「列」はじまり』" = List.FirstN(列名, each Text.StartsWith(_, "列")),
        #"末尾から『長さ1』" = List.LastN(列名, each Text.Length(_) = 1),
        #"末尾から『「列」はじまり』" = List.LastN(列名, each Text.StartsWith(_, "列")),
        #"先頭から『長さ1』" = List.FirstN(列名, each Text.Length(_) = 1)
    ]
in
    // 読みやすさのために整形します。
    Table.TransformColumns(
        Record.ToTable(列名の一部),
        {"Value", Combiner.CombineTextByDelimiter(", ")}
    )
/*
Name    Value
列名  列1, 列2, 列3, 列4, 列5, A, B, C
列名先頭3   列1, 列2, 列3
列名末尾3   A, B, C
列名先頭3以外 列4, 列5, A, B, C
列名末尾3以外 列1, 列2, 列3, 列4, 列5
列名先頭から『「列」はじまり』   列1, 列2, 列3, 列4, 列5
列名末尾から『長さ1』 A, B, C
列名末尾から『「列」はじまり』   
列名先頭から『長さ1』 
*/

C++20&Win API&WIL IShellItemで既知フォルダのフォルダIDと名前を列挙する

IShellItemインターフェイスで既知フォルダ(Known Folder)のフォルダID(識別子)と名前を列挙するコードです。実行するとデバッグウィンドウに既知フォルダの数だけ「フォルダID: 名前」を出力します。

#include <format>
#include <ranges>
#include <vector>

#define STRICT
#define NOMINMAX
#include <Windows.h>
#include <ShlObj.h>

#include "wil/com.h"

// レンジからベクトルを作成します。
template <std::ranges::range Range>
inline constexpr std::vector<std::ranges::range_value_t<Range>> to_vector(const Range& r)
{
    return std::vector(std::ranges::cbegin(r), std::ranges::cend(r));
}

// KNOWNFOLDER_DEFINITION構造体のWILリソースラッパーです。
using unique_knownfolder_definition = wil::unique_struct<
    KNOWNFOLDER_DEFINITION,
    decltype(&FreeKnownFolderDefinitionFields),
    FreeKnownFolderDefinitionFields>;

// wil::com_raw_ptrの型を返します。
template <typename T>
using com_raw_ptr_t = decltype(wil::com_raw_ptr(std::declval<T>()));

// 型Tの変数にwil::com_raw_ptrを適用した戻り値がInterface*型か判定します。
template<typename T, typename Interface>
concept com_raw_ptr_of = std::convertible_to<com_raw_ptr_t<T>, Interface*>;

// IKnownFolderManager::GetFolderIdsを呼び出します。
template<com_raw_ptr_of<IKnownFolderManager> T>
wil::unique_cotaskmem_array_ptr<GUID> GetFolderIds(T& pmanager)
{
    GUID* pfolderIds;
    UINT folderIdCount;
    THROW_IF_FAILED(wil::com_raw_ptr(pmanager)->GetFolderIds(&pfolderIds, &folderIdCount));
    return wil::unique_cotaskmem_array_ptr<GUID>(pfolderIds, folderIdCount);
}

// GUIDをブラケットで挟まれた文字列表現に変換します。
// 失敗時は空の文字列を返します。
inline std::wstring GUIDToWString(const GUID& guid)
{
    const auto size{ std::size(L"{00000000-0000-0000-0000-000000000000}") };
    std::wstring s(size - 1, L'\0');
    return (StringFromGUID2(guid, s.data(), static_cast<int>(size) + 1) != 0)
        ? std::move(s) : std::wstring();
}

int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
{
    // COMを初期化する。
    auto coinit{ wil::CoInitializeEx(COINIT_APARTMENTTHREADED) };

    // KnownFolderManagerのインスタンスを作成する。
    auto manager{ wil::CoCreateInstance<KnownFolderManager, IKnownFolderManager>() };

    // 特殊フォルダのフォルダIDと名前を列挙する。
    wil::unique_cotaskmem_array_ptr<GUID> folderIds{ GetFolderIds(manager) };
    auto view{ folderIds | std::ranges::views::transform(
        [&manager](const auto& folderId)
        {
            wil::com_ptr<IKnownFolder> folder;
            THROW_IF_FAILED(manager->GetFolder(folderId, folder.put()));

            unique_knownfolder_definition definition;
            THROW_IF_FAILED(folder->GetFolderDefinition(&definition));

            return std::make_tuple(folderId, std::wstring(definition.pszName));
        }
    ) };

    // 確認用にデバッガーへ出力します。
    // デバッグ用にstd::vectorにします。
    auto infos{ to_vector(view) };
    for (const auto& info : infos)
    {
        auto s{ std::format(L"{}: {}\n",
            GUIDToWString(std::get<GUID>(info)),
            std::get<std::wstring>(info)) };
        OutputDebugStringW(s.c_str());
    }

    return 0;
}