画像ファイルのタイトルを書き換えるC#サンプルコード「WindowsSearchの有効活用」

コンピュータ

画像素材を大量に管理していると、「あの画像をどこに保存したか分からない」という問題に必ず直面します。
しかし画像ファイルはテキストのように中身を直接検索できないため、実際に使える検索手段はファイル名や拡張子、解像度といった限定的な情報に頼りがちです。

そこで本記事では、Windows Search を有効活用し、画像ファイルを文字情報で検索できるようにする方法を考えます。
具体的には、ファイルのプロパティにある「タイトル」欄に検索キーワードを埋め込み、エクスプローラーの検索窓から全文検索の対象として画像を探せるようにします。
UI は普段使い慣れたエクスプローラーそのまま、素材画像はそのまま D&D で各種アプリに渡せるため、実用面でも相性の良い方法です。

本記事では、その実現手段として
C# から画像ファイルのタイトルを書き換えるサンプルコードを紹介します。

ソースコード

C#でIPropertyStoreを使い画像ファイルのタイトルを書き換えるサンプルコード

ファイル名:ImgTitle.csproj


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

ファイル名:Program.cs


using System;
using System.Runtime.InteropServices;

class ImgTitle
{
    // ===== PROPERTYKEY / PROPVARIANT =====

    [StructLayout(LayoutKind.Sequential)]
    struct PROPERTYKEY
    {
        public Guid fmtid;
        public uint pid;
    }

    [StructLayout(LayoutKind.Explicit)]
    struct PROPVARIANT
    {
        [FieldOffset(0)] public ushort vt;
        [FieldOffset(8)] public IntPtr pwszVal;
    }

    enum VARTYPE : ushort
    {
        VT_EMPTY = 0,
        VT_LPWSTR = 31
    }

    // ===== IPropertyStore =====

    [ComImport]
    [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IPropertyStore
    {
        uint GetCount(out uint cProps);
        uint GetAt(uint iProp, out PROPERTYKEY pkey);
        uint GetValue(ref PROPERTYKEY key, out PROPVARIANT pv);
        uint SetValue(ref PROPERTYKEY key, ref PROPVARIANT pv);
        uint Commit();
    }

    enum GETPROPERTYSTOREFLAGS
    {
        GPS_READWRITE = 0x00000002
    }

    [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
    static extern int SHGetPropertyStoreFromParsingName(
        string pszPath,
        IntPtr pbc,
        GETPROPERTYSTOREFLAGS flags,
        ref Guid riid,
        out IPropertyStore store);

    // ===== System.Title =====

    static readonly PROPERTYKEY PKEY_System_Title = new()
    {
        fmtid = new Guid("F29F85E0-4FF9-1068-AB91-08002B27B3D9"),
        pid = 2
    };

    // ===== エントリポイント =====

    static int Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("Usage:");
            Console.WriteLine("  ImgTitle <file>");
            Console.WriteLine("  ImgTitle <file> \"New Title\"");
            return 1;
        }

        string path = args[0];
        try
        {
            if (args.Length == 1)
            {
                string? title = GetTitle(path);
                if (title == null) return 1;
                Console.WriteLine(title);
            }
            else
            {
                SetTitle(path, args[1]);
            }
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine(ex.Message);
            return 2;
        }

        return 0;
    }

    // ===== 取得 =====

    static string? GetTitle(string path)
    {
        Guid iid = typeof(IPropertyStore).GUID;

        int hr = SHGetPropertyStoreFromParsingName(
            path,
            IntPtr.Zero,
            GETPROPERTYSTOREFLAGS.GPS_READWRITE,
            ref iid,
            out var store);

        if (hr != 0)
            Marshal.ThrowExceptionForHR(hr);

        PROPERTYKEY key = PKEY_System_Title; // ← readonly対策
        store.GetValue(ref key, out var pv);

        if (pv.vt == (ushort)VARTYPE.VT_EMPTY || pv.pwszVal == IntPtr.Zero)
            return null;

        string result = Marshal.PtrToStringUni(pv.pwszVal)!;
        Marshal.FreeCoTaskMem(pv.pwszVal);

        return result;
    }

    // ===== 変更 =====

    static void SetTitle(string path, string title)
    {
        Guid iid = typeof(IPropertyStore).GUID;

        int hr = SHGetPropertyStoreFromParsingName(
            path,
            IntPtr.Zero,
            GETPROPERTYSTOREFLAGS.GPS_READWRITE,
            ref iid,
            out var store);

        if (hr != 0)
            Marshal.ThrowExceptionForHR(hr);

        var pv = new PROPVARIANT
        {
            vt = (ushort)VARTYPE.VT_LPWSTR,
            pwszVal = Marshal.StringToCoTaskMemUni(title)
        };

        PROPERTYKEY key = PKEY_System_Title;
        store.SetValue(ref key, ref pv);
        store.Commit();

        Marshal.FreeCoTaskMem(pv.pwszVal);
    }
}
/*
ビルド方法
dotnet build -c Release -o ./out
*/

実行イメージ

  1. 実行前のファイルのプロパティ

    タイトルは空
  2. 実行
    ImgTitle.exe J:\csharp\console\ImgTitle\sample1.png "PNGのタイトル"

    ※引数のパスはフルパスをセット

  3. 実行後のファイルのプロパティ

    プロパティのタイトルが変更されている。

WindowsSearch

セットしたタイトルの文字列がWindowsSearchの検索対象になります。

エクスプローラーの検索窓にセットした文字を検索するとヒットしました。

※WindowsSearchが無効又は、インデックス対象フォルダでない場合、ヒットしません。

Windows Search の設定

画像ファイルの保存場所(フォルダ)を検索対象にする

本記事の方法を利用するには、画像ファイルが保存されているフォルダが
Windows Search の検索対象(インデックス対象) に含まれている必要があります。

Windows 11 では、以下の手順で設定を確認・変更できます。

  1. 設定 を開く

  2. プライバシーとセキュリティ検索 を選択

  3. 詳細インデックス オプション を開く

「インデックスのオプション」画面が表示されるので、
検索対象の場所 に、画像ファイルを保存しているフォルダが含まれているかを確認します。

含まれていない場合は「変更」をクリックし、
画像素材を保存しているフォルダを追加してください。

この設定が正しく行われていれば、
画像ファイルのプロパティに設定した タイトルコメント
エクスプローラーの検索窓から全文検索の対象になります。


補足:反映されない場合について

フォルダを追加した直後は、検索結果に反映されるまで少し時間がかかることがあります。
すぐに反映されない場合は、同じ画面の 「詳細設定」→「インデックスの再構築」 を実行してください。

画像ファイルの数が多い環境では、再構築に時間がかかる場合があります。

コメント