potisanのプログラミングメモ

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

C# 9.0&Win32 API レジストリからプロパティシステムの拡張子情報を取得する

レジストリからプロパティシステムの拡張子情報を取得するサンプルコードです。情報源はMicrosoftの公式ドキュメントです。

ドキュメントの記載に従い、HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\PropertySystem\PropertyHandlersに登録された拡張子の登録情報をHKEY_CLASSES_ROOT\SystemFileAssociationsから取得しています。ここではPropertyHandlersキーのサブキーに登録されたプロパティハンドラのCLSIDには触れてません。

RegistryPropertyHandlersInfo.cs

#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Win32;

namespace Potisan.Windows.FileSystem
{
    /// <summary>
    /// レジストリに登録されたプロパティハンドラの情報を取得するクラスです。
    /// </summary>
    /// <seealso cref="https://docs.microsoft.com/en-us/windows/win32/search/-search-3x-wds-extidx-propertyhandlers"/>
    public sealed class RegistryPropertyHandlersInfo : IDisposable
    {
        // 繰り返し使用するリソース
        private RegistryKey? systemFileAssocsKey;
        private RegistryKey? propertyHandlersKey;

        public RegistryPropertyHandlersInfo()
        {
            using var hkcr = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64);
            using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
            systemFileAssocsKey = hkcr.OpenSubKey(@"SystemFileAssociations");
            propertyHandlersKey = hklm.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\PropertySystem\PropertyHandlers");
            if (systemFileAssocsKey == null || propertyHandlersKey == null)
                throw new Exception("必要なレジストリキーを開けませんでした。");
        }

        ~RegistryPropertyHandlersInfo()
        {
            Dispose();
        }

        public void Dispose()
        {
            if (IsDisposed)
                return;
            systemFileAssocsKey.Dispose();
            propertyHandlersKey.Dispose();
            systemFileAssocsKey = null;
            propertyHandlersKey = null;
            GC.SuppressFinalize(this);
        }

        [MemberNotNullWhen(false, "systemFileAssocsKey", "propertyHandlersKey")]
        public bool IsDisposed => systemFileAssocsKey == null && propertyHandlersKey == null;

        [MemberNotNull("systemFileAssocsKey", "propertyHandlersKey")]
        private void ThrowIfDisposed()
        {
            if (IsDisposed)
                throw new ObjectDisposedException("RegistryPropertyHandlersInfo");
        }

        public string[] GetExtensionNames()
        {
            ThrowIfDisposed();

            return propertyHandlersKey.GetSubKeyNames()
                .Where(s => s.StartsWith('.'))
                .ToArray();
        }

        public Guid? GetPropertyHandlerCLSID(string extensionName)
        {
            ThrowIfDisposed();

            using var key = propertyHandlersKey.OpenSubKey(extensionName);
            return key?.GetValue("", null) is string s && Guid.TryParseExact(s, "B", out var ret)
                ? ret : null;
        }

        private RegistryKey? OpenExtensionKey(string extensionName)
        {
            ThrowIfDisposed();

            return systemFileAssocsKey.OpenSubKey(@$"{extensionName}") is RegistryKey key1
                ? key1 : null;
        }

        public (string? ExtendedTileInfo, string? FullDetails) GetExtensionRawInfo(string extensionName)
        {
            ThrowIfDisposed();

            using var key = OpenExtensionKey(extensionName);
            if (key == null)
                return (null, null);
            return (key.GetValue("ExtendedTileInfo", null) is string s1 ? s1 : null,
                    key.GetValue("FullDetails", null) is string s2 ? s2 : null);
        }

        public (string[]? ExtendedTileInfo, string[]? FullDetails) GetExtensionInfo(string extensionName)
        {
            ThrowIfDisposed();

            using var key = OpenExtensionKey(extensionName);
            if (key == null)
                return (null, null);
            var (extendedTileInfoRaw, fullDetailsRaw) = GetExtensionRawInfo(extensionName);
            var extendedTileInfo = default(string[]);
            var fullDetails = default(string[]);
            if (extendedTileInfoRaw != null && extendedTileInfoRaw.StartsWith("prop:"))
            {
                extendedTileInfo = extendedTileInfoRaw["prop:".Length..].Split(';');
            }
            if (fullDetailsRaw != null && fullDetailsRaw.StartsWith("prop:"))
            {
                fullDetails = fullDetailsRaw["prop:".Length..].Split(';');
            }
            return (extendedTileInfo, fullDetails);
        }
    }
}

Program.cs

#nullable enable

using System;
using System.Linq;
using Potisan.Windows.FileSystem;

using var propHandlersInfo = new RegistryPropertyHandlersInfo();

var extensionInfos = propHandlersInfo.GetExtensionNames()
    .Select(name =>
    {
        var info = propHandlersInfo.GetExtensionInfo(name);
        return (Name: name, info.ExtendedTileInfo, info.FullDetails);
    });

Console.WriteLine("■ExtendedTileInfo、FullDetailsの登録された拡張子");
var extensionNameWithPSInfo = extensionInfos
    .Where(info => info.ExtendedTileInfo != null || info.FullDetails != null);
foreach (var (name, extendedTileInfo, fullDetails) in extensionNameWithPSInfo)
{
    var s1 = extendedTileInfo is not null ? string.Join<string>(',', extendedTileInfo) : null;
    var s2 = fullDetails is not null ? string.Join<string>(',', fullDetails) : null;
    Console.WriteLine($"{name}, {s1}, {s2}");
}
Console.WriteLine();

Console.WriteLine("■ExtendedTileInfo、FullDetailsの登録されていない拡張子");
var extensionNameWithoutPSInfo = extensionInfos
    .Where(info => info.ExtendedTileInfo == null && info.FullDetails == null)
    .Select(info => info.Name);
Console.WriteLine(string.Join<string>(',', extensionNameWithoutPSInfo));
Console.WriteLine();