ファイル

【C#】拡張子の関連付け情報をレジストリから取得して既定アプリを特定する方法

C#ではWindowsレジストリを参照すれば、指定拡張子に関連付けられた実行ファイルのパスを取得できます。

HKEY_CLASSES_ROOT\.拡張子でProgIDを取り、HKEY_CLASSES_ROOT\ProgID\shell\open\commandの既定値を読めばOKです。

得た文字列から最初の引用符部分を抜き出すと正しいフルパスになり、既定アプリが変更されても同じ手順で追従できます。

レジストリ基礎知識

Windowsのファイル拡張子とそれに関連付けられたアプリケーションの情報は、主にレジストリに保存されています。

C#で拡張子に関連付けられた既定のアプリケーションを特定するには、このレジストリの構造を理解することが重要です。

ここでは、特にHKEY_CLASSES_ROOTキーの役割と、拡張子キーとProgIDキーの関係について詳しく解説します。

HKEY_CLASSES_ROOTの役割

HKEY_CLASSES_ROOT(略してHKCR)は、Windowsのレジストリの中でもファイルの関連付けやCOMオブジェクトの情報を管理する重要なキーです。

ファイルの拡張子に対してどのプログラムが関連付けられているか、またそのプログラムの動作方法などがここに格納されています。

具体的には、HKEY_CLASSES_ROOTは以下の2つのレジストリキーの統合ビューとして機能しています。

  • HKEY_LOCAL_MACHINE\Software\Classes
  • HKEY_CURRENT_USER\Software\Classes

このため、システム全体の設定とユーザーごとの設定が統合されて表示されます。

ユーザーがファイルの関連付けを変更した場合は、HKEY_CURRENT_USER\Software\Classesに設定が保存され、HKEY_CLASSES_ROOTからはそのユーザー設定が優先的に参照されます。

この仕組みにより、ユーザーごとに異なる既定のアプリケーション設定が可能となっています。

拡張子キーとProgIDキーの関係

HKEY_CLASSES_ROOTの中には、拡張子を表すキー(例:.txt.jpg)と、ProgID(プログラム識別子)を表すキーが存在します。

拡張子キーは、どのProgIDに関連付けられているかを示す役割を持ち、ProgIDキーはそのプログラムの詳細な動作情報を持っています。

既定値に格納される情報の意味

拡張子キー(例:.txt)の(既定)値には、その拡張子に関連付けられたProgIDが文字列として格納されています。

ProgIDは、プログラムやファイルタイプを識別するための名前で、例えばtxtfilejpegfileなどがよく使われます。

この値が空や存在しない場合は、その拡張子に関連付けられたプログラムが設定されていないことを意味します。

ProgIDは、HKEY_CLASSES_ROOTのルート直下にあるキーとして存在し、その中にファイルの動作に関する詳細な情報が格納されています。

shell\open\commandキーが示す実行コマンド

ProgIDキーの中には、shell\open\commandというサブキーがあります。

このキーの(既定)値には、そのファイルタイプを開くための実行コマンドが文字列として格納されています。

このコマンドは、実際にファイルを開く際に呼び出されるプログラムのパスや引数を含んでいます。

例えば、.txtファイルのtxtfile ProgIDのshell\open\commandの値は、以下のような文字列になることがあります。

"C:\Windows\System32\notepad.exe" "%1" ここで"%1"は、開くファイルのパスが渡されるプレースホルダーです。

引用符で囲まれているのは、ファイルパスに空白が含まれていても正しく認識されるようにするためです。

このコマンド文字列を解析することで、どのプログラムがその拡張子のファイルを開くのかを特定できます。

このように、HKEY_CLASSES_ROOTの拡張子キーからProgIDを取得し、ProgIDのshell\open\commandキーから実行コマンドを読み取ることで、拡張子に関連付けられた既定のアプリケーションを特定できます。

C#でこれらの情報を取得する際は、これらのレジストリキーを順に参照していくことが基本的な流れとなります。

Windowsでの拡張子―アプリ関連付けの仕組み

UserChoiceキーによるユーザー設定の優先順位

Windowsでは、ファイルの拡張子に対する既定のアプリケーション設定は、システム全体の設定とユーザーごとの設定が存在します。

ユーザーがエクスプローラーの「プログラムから開く」や「既定のアプリの選択」などで関連付けを変更した場合、その情報はレジストリのUserChoiceキーに保存されます。

具体的には、HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\<拡張子>\UserChoiceに以下のような値が格納されています。

  • ProgId:ユーザーが選択したProgID
  • Hash:設定の改ざんを防ぐためのハッシュ値

このUserChoiceキーが存在し、かつ有効な場合は、HKEY_CLASSES_ROOTの拡張子キーに設定されているProgIDよりも優先されます。

つまり、ユーザーが明示的に関連付けを変更していれば、その設定が最優先で使われる仕組みです。

ただし、UserChoiceキーのHashはWindowsが内部的に生成しており、改ざんを防止するために存在します。

直接このキーを編集することは推奨されておらず、APIや設定画面を通じて変更するのが正しい方法です。

C#で拡張子の既定アプリを取得する際は、まずUserChoiceキーのProgIdを確認し、存在しなければHKEY_CLASSES_ROOTの拡張子キーの既定値を参照する流れが一般的です。

32ビット・64ビット環境でのリダイレクト

Windowsのレジストリは、32ビットアプリケーションと64ビットアプリケーションでアクセスするキーが異なる場合があります。

これはWOW64(Windows 32-bit on Windows 64-bit)という互換レイヤーによるもので、32ビットアプリが64ビット用のレジストリキーに誤ってアクセスしないようにリダイレクトが行われています。

具体的には、64ビットWindows上で32ビットアプリがHKEY_LOCAL_MACHINE\SoftwareHKEY_CLASSES_ROOTを参照すると、実際にはHKEY_LOCAL_MACHINE\Software\WOW6432NodeHKEY_CLASSES_ROOT\WOW6432Nodeにリダイレクトされることがあります。

このため、C#のアプリケーションが32ビットでビルドされている場合、64ビット用の関連付け情報を正しく取得できないことがあります。

特に、64ビット版の既定アプリ設定はWOW6432Nodeの下には存在しないため、32ビットアプリからは見えないことがあります。

これを回避するには、Microsoft.Win32.RegistryKeyOpenBaseKeyメソッドとRegistryView列挙体を使い、明示的に64ビットビューまたは32ビットビューのレジストリを指定して開く方法があります。

  • 64ビットレジストリビューを指定して開く場合

RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64)

  • 32ビットレジストリビューを指定して開く場合

RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry32) 通常は64ビット環境で64ビットの既定アプリ情報を取得したい場合、64ビットビューでレジストリを開く必要があります。

このリダイレクトの仕組みを理解し、適切なレジストリビューを指定しないと、拡張子の関連付け情報が取得できなかったり、誤った情報を取得したりする原因となります。

C#からレジストリを読む準備

Microsoft.Win32名前空間の主要クラス

C#でWindowsのレジストリを操作する際は、Microsoft.Win32名前空間に含まれるクラスを利用します。

特に重要なのはRegistryクラスとRegistryKeyクラスです。

これらを適切に使い分けることで、レジストリの読み書きを効率的かつ安全に行えます。

RegistryKeyとRegistryクラスの使い分け

Registryクラスは、レジストリのルートキー(HKEY_CLASSES_ROOTHKEY_CURRENT_USERなど)を表す静的プロパティを提供しています。

例えば、Registry.ClassesRootRegistry.CurrentUserなどです。

これらはルートキーを表すRegistryKeyオブジェクトを返します。

一方、RegistryKeyクラスは、レジストリキーの開閉、値の読み書き、サブキーの列挙などの操作を行うためのインスタンスクラスです。

Registryクラスの静的プロパティやメソッドで取得したRegistryKeyオブジェクトを使って、さらに深い階層のキーを開いたり、値を取得したりします。

使い分けのポイントは以下の通りです。

  • ルートキーを取得する際はRegistryクラスの静的プロパティを使います
  • ルートキー以下のキーを操作する際はRegistryKeyOpenSubKeyCreateSubKeyを使います
  • 値の読み書きはRegistryKeyGetValueSetValueを使います

例えば、HKEY_CLASSES_ROOT\.txtキーの既定値を取得する場合は、Registry.ClassesRoot.OpenSubKey(".txt")でキーを開き、GetValue("")で既定値を取得します。

開放忘れによるリーク防止策

RegistryKeyはアンマネージドリソースを扱うため、使い終わったら必ずCloseメソッドを呼んで開放する必要があります。

開放を忘れると、リソースリークやハンドルリークが発生し、アプリケーションの安定性に悪影響を及ぼします。

C#ではIDisposableインターフェースを実装しているため、usingステートメントを使うのが推奨されます。

usingを使うと、スコープを抜けた際に自動的にDispose(内部でCloseを呼ぶ)が実行され、確実にリソースが解放されます。

using (var key = Registry.ClassesRoot.OpenSubKey(".txt"))
{
    var value = key?.GetValue("") as string;
    Console.WriteLine(value);
}
// usingブロックを抜けると自動的にkeyが閉じられる

このようにusingを使うことで、開放忘れを防止し、コードの可読性も向上します。

アプリ実行権限とUACの影響

Windowsのユーザーアカウント制御(UAC)により、レジストリの一部キーは管理者権限がないとアクセスできない場合があります。

C#アプリケーションがレジストリを読み取る際に権限不足で例外が発生することもあるため、権限の扱いに注意が必要です。

参照のみであれば管理者権限が不要なケース

拡張子の関連付け情報を取得するだけであれば、基本的に読み取り操作のみとなるため、管理者権限は不要です。

HKEY_CLASSES_ROOTはシステム全体の設定を含みますが、読み取りは標準ユーザー権限でも可能です。

ただし、ユーザーごとの設定が格納されているHKEY_CURRENT_USERFileExtsキーなども参照する場合は、同様に読み取り権限があれば問題ありません。

逆に、書き込みや変更を行う場合は管理者権限が必要になることが多いので、読み取り専用の用途であればUACの昇格は不要と考えてよいです。

WOW6432Nodeとリダイレクト回避

32ビットアプリケーションが64ビットWindows上でレジストリを操作すると、WOW6432Nodeというサブキーにリダイレクトされることがあります。

これにより、64ビット用の設定が見えなくなったり、誤った情報を取得したりすることがあります。

この問題を回避するには、RegistryKey.OpenBaseKeyメソッドとRegistryView列挙体を使い、64ビットビューまたは32ビットビューを明示的に指定してレジストリを開く方法があります。

using Microsoft.Win32;
var baseKey64 = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64);
using (var key = baseKey64.OpenSubKey(".txt"))
{
    var progId = key?.GetValue("") as string;
    Console.WriteLine(progId);
}
txtfilelegacy

このように64ビットビューを指定すれば、64ビット用の関連付け情報を正しく取得できます。

逆に32ビットビューを指定すれば、32ビット用の情報を取得可能です。

アプリケーションのビルドプラットフォームや対象環境に応じて、適切なビューを選択することが重要です。

拡張子からProgIDを取得するロジック

GetValueメソッドで既定値を読む流れ

拡張子に関連付けられたProgIDを取得するには、まずHKEY_CLASSES_ROOTの該当拡張子キーの既定値を読み取る必要があります。

C#ではRegistryKeyクラスのGetValueメソッドを使って、キーの既定値を取得します。

具体的な流れは以下の通りです。

  1. RegistryKey.OpenBaseKeyHKEY_CLASSES_ROOTを開き、適切なRegistryView(64ビットまたは32ビット)を指定します。
  2. OpenSubKeyで拡張子(例:.txt)のキーを開きます。
  3. GetValueメソッドに空文字列""を渡して既定値を取得します。

この既定値がProgIDに該当します。

ProgIDは文字列で、例えばtxtfilejpegfileなどの名前が返されます。

以下はサンプルコードです。

using Microsoft.Win32;
using System;
class Program
{
    static void Main()
    {
        string extension = ".txt";
        // 64ビットレジストリビューでHKEY_CLASSES_ROOTを開く
        using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64))
        using (var extKey = baseKey.OpenSubKey(extension))
        {
            if (extKey == null)
            {
                Console.WriteLine($"拡張子 {extension} のキーが見つかりません。");
                return;
            }
            // 既定値を取得(ProgID)
            var progId = extKey.GetValue("") as string;
            if (!string.IsNullOrEmpty(progId))
            {
                Console.WriteLine($"拡張子 {extension} に関連付けられたProgID: {progId}");
            }
            else
            {
                Console.WriteLine($"拡張子 {extension} に関連付けられたProgIDが存在しません。");
            }
        }
    }
}
拡張子 .txt に関連付けられたProgID: txtfilelegacy

このコードは、指定した拡張子のキーが存在しない場合や、既定値が空の場合に適切にメッセージを表示します。

GetValueメソッドはキーが存在しない場合はnullを返すため、nullチェックも重要です。

ProgIDが存在しない場合のフォールバック処理

拡張子キーの既定値にProgIDが設定されていないケースもあります。

この場合、関連付けが未設定か、特殊なファイルタイプである可能性があります。

こうした場合に備えてフォールバック処理を用意すると、より堅牢な実装になります。

代表的なフォールバックの方法は以下の通りです。

  • 拡張子キーのUserChoiceサブキーを確認する

ユーザーが関連付けを変更している場合、HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\<拡張子>\UserChoiceProgId値に設定があることがあります。

ここを参照してみるのも有効です。

  • 拡張子キーの既定値が空でも、PerceivedTypeContent Typeなどの値を参考にして、ファイルタイプを推測する

これらの値は拡張子キーに設定されていることがあり、ファイルの種類を示します。

  • それでも情報が得られない場合は、関連付けなしとして処理する

以下はUserChoiceキーを参照する例です。

using Microsoft.Win32;
using System;
class Program
{
    static void Main()
    {
        string extension = ".txt";
        // まずHKEY_CURRENT_USERのUserChoiceを確認
        string userChoicePath = $@"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{extension}\UserChoice";
        using (var userChoiceKey = Registry.CurrentUser.OpenSubKey(userChoicePath))
        {
            if (userChoiceKey != null)
            {
                var userProgId = userChoiceKey.GetValue("ProgId") as string;
                if (!string.IsNullOrEmpty(userProgId))
                {
                    Console.WriteLine($"UserChoiceで設定されたProgID: {userProgId}");
                    return;
                }
            }
        }
        // UserChoiceがなければHKEY_CLASSES_ROOTを参照
        using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64))
        using (var extKey = baseKey.OpenSubKey(extension))
        {
            var progId = extKey?.GetValue("") as string;
            if (!string.IsNullOrEmpty(progId))
            {
                Console.WriteLine($"HKEY_CLASSES_ROOTで設定されたProgID: {progId}");
            }
            else
            {
                Console.WriteLine($"拡張子 {extension} に関連付けられたProgIDが見つかりません。");
            }
        }
    }
}
UserChoiceで設定されたProgID: VSCode.txt

このように、UserChoiceキーを優先的に確認し、存在しなければHKEY_CLASSES_ROOTの拡張子キーを参照することで、ユーザー設定を尊重しつつ既定の関連付け情報を取得できます。

フォールバック処理を入れることで、より正確に拡張子の関連付け情報を取得できるようになります。

ProgIDから実行コマンドを取得するロジック

shell\open\commandキーの読み取り

ProgIDから実際にファイルを開くための実行コマンドを取得するには、HKEY_CLASSES_ROOT\<ProgID>\shell\open\commandキーの既定値を読み取ります。

このキーには、関連付けられたプログラムの実行パスや引数が文字列として格納されています。

例えば、txtfileというProgIDの場合、shell\open\commandの値は以下のようになっていることがあります。

"C:\Windows\System32\notepad.exe" "%1" この文字列は、ファイルを開く際に実行されるコマンドラインを示しています。

%1プレースホルダーの扱い

%1は、実行コマンド内で開くファイルのパスを表すプレースホルダーです。

Windowsはこの部分に実際のファイルパスを挿入してコマンドを実行します。

C#でこの文字列を取得した場合、%1はそのまま文字列に含まれています。

実際にプログラムのパスを特定したい場合は、%1を含む部分を除去するか、コマンドラインの先頭部分だけを抽出する必要があります。

また、%1以外にも%L%*などのプレースホルダーが使われることがありますが、基本的にはファイルパスや複数ファイルを表すものです。

空白文字と引用符のパース

実行コマンド文字列には、プログラムのパスや引数に空白が含まれることが多いため、パスは引用符"で囲まれていることが一般的です。

例えば、

"C:\Program Files\MyApp\app.exe" "%1" のように記述されます。

C#でこの文字列から実行ファイルのパスを抽出する際は、単純に空白で分割すると誤った結果になることがあります。

引用符の内側は一つのトークンとして扱う必要があります。

安全な方法としては、以下のような処理が考えられます。

  • 文字列の先頭から最初の引用符で囲まれた部分を抽出する
  • 引用符がない場合は、最初の空白までの文字列を実行ファイルパスとみなす
  • コマンド文字列:"C:\Program Files\MyApp\app.exe" "%1"
    • 実行ファイルパス:C:\Program Files\MyApp\app.exe
  • コマンド文字列:C:\Windows\System32\notepad.exe "%1"
    • 実行ファイルパス:C:\Windows\System32\notepad.exe

このように引用符の有無を考慮してパースすることが重要です。

AssocQueryString APIとの比較

Windows APIのAssocQueryString関数は、拡張子やProgIDに関連付けられた情報を取得するための公式なAPIです。

ASSOCFフラグを指定して、既定のプログラムの実行コマンドや表示名などを取得できます。

AssocQueryStringを使う利点は以下の通りです。

  • レジストリの複雑な構造を意識せずに済む
  • ユーザーのUserChoice設定やシステムの優先順位を自動的に考慮する
  • 64ビット・32ビットのリダイレクトを気にせずに使える
  • 将来的なWindowsの仕様変更にも対応しやすい

一方で、C#から直接AssocQueryStringを使うにはP/Invokeでの宣言が必要で、やや手間がかかります。

また、APIの戻り値の処理やバッファ管理も注意が必要です。

レジストリを直接読む方法は、APIを使わずに済むため手軽ですが、UserChoiceキーの優先順位やリダイレクトの考慮などを自分で実装する必要があります。

まとめると、

比較項目レジストリ直接読み取りAssocQueryString API
実装の手軽さ簡単だが細かい考慮が必要P/Invokeが必要でやや複雑
ユーザー設定の反映自分でUserChoiceを考慮する必要あり自動的に考慮される
64/32ビット対応明示的にビュー指定が必要自動的に対応
将来の互換性Windowsの仕様変更に弱いことがある公式APIなので互換性が高い

用途や環境に応じて使い分けるのが望ましいです。

簡単なツールや学習目的ならレジストリ直接読み取りで十分ですが、信頼性や将来性を重視する場合はAssocQueryString APIの利用を検討してください。

実装時に気を付けたいポイント

例外パターンと対策

レジストリを操作する際は、さまざまな例外が発生する可能性があります。

特に読み取り時でも、アクセス権限や環境によって例外が起きることがあるため、適切な例外処理を行うことが重要です。

SecurityException

SecurityExceptionは、アプリケーションにレジストリへのアクセス権限が不足している場合に発生します。

特に、サンドボックス環境や制限付きのユーザーアカウントで実行している場合に起こりやすいです。

対策としては、以下の点に注意してください。

  • 読み取り専用であっても、アクセス権限が必要なキーがあることを理解します
  • 可能な限り、管理者権限を要求しない設計にします
  • 例外発生時はユーザーに権限不足を通知し、必要に応じて権限昇格を促します
  • 例外をキャッチして処理を継続できるようにし、アプリケーションのクラッシュを防ぐ
try
{
    using (var key = Registry.ClassesRoot.OpenSubKey(".txt"))
    {
        var progId = key?.GetValue("") as string;
        Console.WriteLine(progId);
    }
}
catch (System.Security.SecurityException ex)
{
    Console.WriteLine("レジストリへのアクセス権限が不足しています。管理者権限で実行してください。");
}

IOException・UnauthorizedAccessException

IOExceptionは、レジストリキーが破損している、またはアクセス中に問題が発生した場合に起こることがあります。

UnauthorizedAccessExceptionは、読み取り権限がない場合に発生します。

対策としては、

  • キーが存在しない場合やアクセスできない場合に備え、nullチェックや例外処理を行います
  • 例外が発生してもアプリケーションが継続できるように、適切にキャッチしてログ出力やユーザー通知を行います
  • 可能ならば、アクセス権限のあるキーのみを対象にします
try
{
    using (var key = Registry.ClassesRoot.OpenSubKey(".txt"))
    {
        if (key == null)
        {
            Console.WriteLine("指定した拡張子のキーが存在しません。");
            return;
        }
        var progId = key.GetValue("") as string;
        Console.WriteLine(progId);
    }
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("レジストリの読み取り権限がありません。");
}
catch (IOException)
{
    Console.WriteLine("レジストリへのアクセス中にエラーが発生しました。");
}

ネットワークドライブ・UNCパス対応

拡張子に関連付けられたプログラムの実行コマンドには、ネットワークドライブやUNCパス(例:\\server\share\app.exe)が指定されている場合があります。

これらのパスはローカルドライブとは異なる扱いが必要です。

注意点は以下の通りです。

  • ネットワークパスが存在しない、またはアクセスできない場合、プログラムの起動に失敗する可能性があります
  • UNCパスは、ファイルパスの検証や存在確認を行う際に特別な処理が必要な場合があります
  • ネットワーク環境によってはアクセス遅延やタイムアウトが発生することがあります

C#での対策例としては、System.IO.PathクラスのIsPathRootedUriクラスを使ってパスの種類を判別し、ネットワークパスの場合はアクセス可能かどうかを事前にチェックする方法があります。

string programPath = @"\\server\share\app.exe";
if (Uri.TryCreate(programPath, UriKind.Absolute, out Uri uri) && uri.IsUnc)
{
    Console.WriteLine("ネットワークパスが指定されています。アクセス可能か確認してください。");
    // ここでファイルの存在チェックやアクセス権限の確認を行う
}
else
{
    Console.WriteLine("ローカルパスです。通常通り処理します。");
}

パフォーマンス最適化のヒント

レジストリの読み取りは比較的高速ですが、頻繁にアクセスしたり大量の拡張子を処理したりする場合はパフォーマンスに影響が出ることがあります。

以下の工夫で効率化を図れます。

キャッシュ戦略

一度取得した拡張子とProgID、実行コマンドの情報をメモリ上にキャッシュしておくと、同じ情報を何度もレジストリから読み取る必要がなくなります。

キャッシュのポイントは、

  • 拡張子ごとに一意のキーでキャッシュを管理します
  • キャッシュの有効期限や更新タイミングを設けます
  • ユーザーが関連付けを変更した場合にキャッシュをクリアまたは更新する仕組みを用意します

例えば、Dictionary<string, string>で拡張子をキーにしてProgIDや実行コマンドを保存する方法があります。

レジストリ監視によるリアルタイム反映

ユーザーが既定のアプリを変更した場合、レジストリの関連キーが更新されます。

これを検知してキャッシュを更新するには、Windowsのレジストリ監視機能を利用します。

C#ではRegNotifyChangeKeyValue APIをP/Invokeで呼び出すか、ManagementEventWatcherクラスを使ってWMIイベントを監視する方法があります。

これにより、関連付けの変更をリアルタイムに検知し、キャッシュを即座に更新できるため、常に最新の情報を提供できます。

ただし、監視処理はリソースを消費するため、必要な場合に限定して実装するのが望ましいです。

既定アプリ変更への対応

UserChoiceキー変更監視のアイデア

Windowsでは、ユーザーがファイルの既定アプリを変更すると、UserChoiceキーの内容が更新されます。

具体的には、HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\<拡張子>\UserChoiceProgId値が変わることで、関連付けが切り替わります。

この変更を検知してアプリケーション側で既定アプリの変化に対応するには、レジストリのUserChoiceキーの監視が有効です。

C#での監視方法のアイデアは以下の通りです。

  • RegNotifyChangeKeyValue APIを使う

Windows APIのRegNotifyChangeKeyValueをP/Invokeで呼び出し、指定したレジストリキーの変更を非同期または同期で監視します。

これにより、UserChoiceキーの変更をリアルタイムに検知可能です。

  • 監視対象のキーを動的に切り替える

複数の拡張子を監視する場合は、それぞれのUserChoiceキーを監視対象に設定し、変更があったら対応する拡張子の情報を更新します。

  • 監視スレッドの設計

監視はブロッキング呼び出しになるため、別スレッドで実行し、変更検知時にメインスレッドへ通知する仕組みを作ると良いです。

  • 監視解除とリソース管理

アプリケーション終了時や監視不要時には、必ず監視を解除し、リソースを解放することが重要です。

この方法でUserChoiceキーの変更を検知すれば、ユーザーが既定アプリを変更したタイミングで即座に情報を更新し、最新の関連付け情報を反映できます。

ファイル関連付けイベントをフックする方法

Windowsはファイル関連付けの変更をシステムイベントとして通知する仕組みも持っています。

これを利用して、既定アプリの変更を検知する方法もあります。

代表的な方法は以下の通りです。

  • WMIイベントの監視

ManagementEventWatcherクラスを使い、WMIの__InstanceModificationEventを監視して、UserChoiceキーや関連するレジストリキーの変更を検知します。

これにより、レジストリ監視よりも高レベルでのイベント通知が可能です。

  • Windowsメッセージのフック

エクスプローラーやシェルが送信するWindowsメッセージ(例:WM_SETTINGCHANGE)をアプリケーションで受け取り、関連付けの変更を検知する方法です。

ただし、この方法は全ての変更を確実に捕捉できるわけではありません。

  • ファイル関連付けAPIの利用

一部のAPIやCOMインターフェースを利用して、関連付けの変更通知を受け取る方法もありますが、実装が複雑になるため用途に応じて検討します。

これらの方法を組み合わせることで、ユーザーが既定アプリを変更した際にアプリケーション側で即座に反応し、UIの更新や内部データの再取得を行うことが可能です。

ただし、イベント監視はリソースを消費するため、必要な範囲で効率的に実装することが望ましいです。

サンプルコード解説

拡張子→実行ファイルパス取得関数

拡張子から関連付けられた実行ファイルのパスを取得するには、まず拡張子に対応するProgIDをレジストリから取得し、そのProgIDのshell\open\commandキーから実行コマンドを読み取ります。

以下の関数は、この一連の処理をまとめた例です。

using Microsoft.Win32;
using System;
using System.Text.RegularExpressions;
public static class FileAssociationHelper
{
    /// <summary>
    /// 指定した拡張子に関連付けられた実行ファイルのパスを取得します。
    /// </summary>
    /// <param name="extension">拡張子(例: ".txt")</param>
    /// <returns>実行ファイルのフルパス。関連付けがない場合はnull。</returns>
    public static string GetAssociatedExecutablePath(string extension)
    {
        if (string.IsNullOrWhiteSpace(extension))
            throw new ArgumentException("拡張子を指定してください。", nameof(extension));
        if (!extension.StartsWith("."))
            extension = "." + extension;
        try
        {
            // UserChoiceキーを優先的に確認
            string userChoicePath = $@"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\{extension}\UserChoice";
            using (var userChoiceKey = Registry.CurrentUser.OpenSubKey(userChoicePath))
            {
                if (userChoiceKey != null)
                {
                    var userProgId = userChoiceKey.GetValue("ProgId") as string;
                    if (!string.IsNullOrEmpty(userProgId))
                    {
                        var path = GetCommandPathFromProgId(userProgId);
                        if (!string.IsNullOrEmpty(path))
                            return path;
                    }
                }
            }
            // UserChoiceがなければHKEY_CLASSES_ROOTから取得
            using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64))
            using (var extKey = baseKey.OpenSubKey(extension))
            {
                var progId = extKey?.GetValue("") as string;
                if (string.IsNullOrEmpty(progId))
                    return null;
                return GetCommandPathFromProgId(progId);
            }
        }
        catch
        {
            // 例外は呼び出し元で処理するためここではnullを返す
            return null;
        }
    }
    /// <summary>
    /// ProgIDからshell\open\commandの実行ファイルパスを取得します。
    /// </summary>
    /// <param name="progId">ProgID文字列</param>
    /// <returns>実行ファイルのパス。取得できなければnull。</returns>
    private static string GetCommandPathFromProgId(string progId)
    {
        if (string.IsNullOrEmpty(progId))
            return null;
        using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64))
        using (var commandKey = baseKey.OpenSubKey($@"{progId}\shell\open\command"))
        {
            var command = commandKey?.GetValue("") as string;
            if (string.IsNullOrEmpty(command))
                return null;
            // コマンド文字列から実行ファイルパスを抽出
            return ExtractExecutablePath(command);
        }
    }
    /// <summary>
    /// コマンドライン文字列から実行ファイルパスを抽出します。
    /// </summary>
    /// <param name="command">コマンドライン文字列</param>
    /// <returns>実行ファイルパス</returns>
    private static string ExtractExecutablePath(string command)
    {
        if (string.IsNullOrEmpty(command))
            return null;
        // 正規表現で引用符で囲まれたパスを抽出
        var match = Regex.Match(command, "^\"([^\"]+)\"");
        if (match.Success)
            return match.Groups[1].Value;
        // 引用符がなければ空白までをパスとみなす
        var parts = command.Split(' ');
        return parts.Length > 0 ? parts[0] : null;
    }
}

この関数は、まずUserChoiceキーを優先的に参照し、ユーザーが設定した関連付けを尊重します。

次にHKEY_CLASSES_ROOTの拡張子キーからProgIDを取得し、さらにそのProgIDのshell\open\commandキーから実行コマンドを読み取ります。

コマンド文字列からは正規表現を使って実行ファイルのパスを抽出しています。

GUIアプリでの利用例

WindowsフォームやWPFなどのGUIアプリケーションで、ユーザーが指定したファイルの拡張子に関連付けられた実行ファイルパスを取得し、表示や処理に活用する例を示します。

ドラッグ&ドロップ対応

ファイルをドラッグ&ドロップで受け取り、その拡張子に関連付けられた実行ファイルパスを表示する簡単な例です。

using System;
using System.Windows.Forms;
public class MainForm : Form
{
    private TextBox textBox;
    private Label label;
    public MainForm()
    {
        this.Text = "拡張子関連付け取得デモ";
        this.Width = 400;
        this.Height = 150;
        label = new Label
        {
            Text = "ファイルをドラッグ&ドロップしてください",
            Dock = DockStyle.Top,
            Height = 30,
            TextAlign = System.Drawing.ContentAlignment.MiddleCenter
        };
        this.Controls.Add(label);
        textBox = new TextBox
        {
            Dock = DockStyle.Fill,
            ReadOnly = true,
            Multiline = true
        };
        this.Controls.Add(textBox);
        this.AllowDrop = true;
        this.DragEnter += MainForm_DragEnter;
        this.DragDrop += MainForm_DragDrop;
    }
    private void MainForm_DragEnter(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop))
            e.Effect = DragDropEffects.Copy;
        else
            e.Effect = DragDropEffects.None;
    }
    private void MainForm_DragDrop(object sender, DragEventArgs e)
    {
        var files = (string[])e.Data.GetData(DataFormats.FileDrop);
        if (files.Length == 0)
            return;
        string filePath = files[0];
        string extension = System.IO.Path.GetExtension(filePath);
        string exePath = FileAssociationHelper.GetAssociatedExecutablePath(extension);
        if (exePath != null)
            textBox.Text = $"拡張子: {extension}\r\n関連付けられた実行ファイル:\r\n{exePath}";
        else
            textBox.Text = $"拡張子: {extension}\r\n関連付けられた実行ファイルが見つかりません。";
    }
}

このフォームは、ファイルをドラッグ&ドロップすると、そのファイルの拡張子を取得し、先ほどのFileAssociationHelperクラスの関数を使って関連付けられた実行ファイルパスを表示します。

マルチスレッドでの呼び出し注意点

GUIアプリケーションでは、UIスレッドと別のスレッドで処理を分けることが多いです。

レジストリの読み取りは比較的軽量ですが、複数ファイルの処理や頻繁な呼び出しがある場合は、UIの応答性を保つために別スレッドで実行することが望ましいです。

ただし、UIコントロールの更新はUIスレッドで行う必要があるため、以下の点に注意してください。

  • レジストリ読み取りはTask.RunBackgroundWorkerなどで非同期に実行します
  • 結果をUIに反映する際は、InvokeBeginInvokeを使ってUIスレッドに切り替えます
  • 例外処理も非同期処理内で適切に行い、UIに影響を与えないようにします
private async void MainForm_DragDrop(object sender, DragEventArgs e)
{
    var files = (string[])e.Data.GetData(DataFormats.FileDrop);
    if (files.Length == 0)
        return;
    string filePath = files[0];
    string extension = System.IO.Path.GetExtension(filePath);
    string exePath = null;
    try
    {
        exePath = await System.Threading.Tasks.Task.Run(() =>
            FileAssociationHelper.GetAssociatedExecutablePath(extension));
    }
    catch (Exception ex)
    {
        exePath = null;
        // ログ出力やエラーハンドリング
    }
    this.Invoke((Action)(() =>
    {
        if (exePath != null)
            textBox.Text = $"拡張子: {extension}\r\n関連付けられた実行ファイル:\r\n{exePath}";
        else
            textBox.Text = $"拡張子: {extension}\r\n関連付けられた実行ファイルが見つかりません。";
    }));
}

このように非同期処理とUIスレッドの切り替えを適切に行うことで、ユーザー操作中のUIのフリーズを防ぎ、快適な操作感を実現できます。

Office系拡張子で複数バージョンが入っている場合

Microsoft Officeのような大規模なアプリケーションは、複数バージョンが同じPCにインストールされていることがあります。

この場合、.docx.xlsxなどのOffice系拡張子に関連付けられたProgIDや実行コマンドが複数存在し、どのバージョンが既定アプリとして設定されているかが問題になります。

Windowsの既定アプリ設定は、ユーザーが最後に関連付けを変更したバージョンを優先します。

レジストリ上では、UserChoiceキーに設定されたProgIDが優先されるため、複数バージョンがあっても基本的にはユーザー設定が反映されます。

ただし、以下の点に注意が必要です。

  • ProgIDの命名規則がバージョンごとに異なる

例えば、Office 2016はWord.Document.16、Office 2013はWord.Document.15のようにバージョン番号が含まれています。

これにより、どのバージョンが関連付けられているかをProgIDから判別可能です。

  • レジストリのUserChoiceキーが存在しない場合

システムの既定設定が使われるため、古いバージョンのProgIDが返ることがあります。

  • Officeのアップデートや修復で関連付けが変わることがある

Officeの修復やアップデート時に関連付けがリセットされることがあるため、常に最新の状態を取得したい場合はUserChoiceキーを優先的に参照することが重要です。

  • 64ビット版と32ビット版の違い

Officeのビット数によっても関連付け情報が異なる場合があるため、64ビット環境で32ビットOfficeがインストールされている場合は、レジストリビューの指定に注意が必要です。

このように、Office系拡張子の関連付けを正確に取得するには、UserChoiceキーの存在を確認し、ProgIDのバージョン番号を解析することがポイントとなります。

ストアアプリが既定になっている場合

Windows 10以降では、Microsoft StoreからインストールしたUWP(ユニバーサルWindowsプラットフォーム)アプリがファイルの既定アプリとして設定されることがあります。

この場合、レジストリの関連付け情報が従来のデスクトップアプリとは異なる形式で管理されているため、注意が必要です。

主な特徴は以下の通りです。

  • ProgIDがAppXで始まる形式になる

ストアアプリの関連付けはAppXで始まるProgIDが割り当てられ、実行コマンドも特殊な形式で格納されています。

  • 実行コマンドが直接の実行ファイルパスではない

ストアアプリはパッケージ化されているため、shell\open\commandキーの値が存在しないか、explorer.exeを介して起動されることがあります。

  • AssocQueryString APIの利用が推奨される

ストアアプリの関連付け情報はレジストリを直接読むだけでは正確に取得できない場合が多いため、WindowsのAssocQueryString APIを使うことで正しい既定アプリ情報を取得しやすくなります。

  • 起動方法が異なるため、実行ファイルパスの取得が困難

ストアアプリはパッケージIDやアプリケーションユーザーID(AUMID)で管理されているため、単純に実行ファイルパスを取得することはできません。

このため、ストアアプリが既定になっている場合は、レジストリからの直接取得に加えてAPIの利用や別の方法で起動情報を取得する必要があります。

企業環境でのレジストリポリシー影響

企業の管理された環境では、グループポリシーや管理用テンプレートによってレジストリのファイル関連付け設定が制限されていることがあります。

これにより、以下のような影響が考えられます。

  • ユーザーが既定アプリを変更できない

ポリシーで関連付けの変更が禁止されている場合、UserChoiceキーの変更が制限され、システム管理者が設定した既定アプリが強制されます。

  • レジストリの特定キーが読み取り専用または非表示になる

アクセス権限が制限されているため、アプリケーションが関連付け情報を取得しようとしてもUnauthorizedAccessExceptionSecurityExceptionが発生することがあります。

  • ポリシーによる関連付けの上書き

グループポリシーで設定された関連付けがUserChoiceよりも優先される場合があり、通常のユーザー設定が反映されないことがあります。

  • 管理用ツールやスクリプトでの一括設定

企業環境では、管理者がPowerShellスクリプトや専用ツールで関連付けを一括設定していることが多く、これらの設定がレジストリに反映されます。

このような環境では、アプリケーション側で関連付け情報を取得する際に例外処理を強化し、権限不足や設定の不整合に対応する必要があります。

また、管理者に問い合わせてポリシーの内容を確認することも重要です。

まとめ

この記事では、C#で拡張子に関連付けられた既定アプリをレジストリから取得する方法を解説しました。

HKEY_CLASSES_ROOTの拡張子キーからProgIDを取得し、ProgIDのshell\open\commandキーから実行コマンドを読み取る流れや、ユーザー設定を反映するUserChoiceキーの扱い、32/64ビット環境の注意点も説明しています。

例外処理やパフォーマンス最適化、既定アプリ変更の監視方法も紹介し、実用的なサンプルコードも掲載しました。

これにより、Windowsの関連付け情報を正確に取得し、柔軟に活用できる知識が身につきます。

関連記事

Back to top button
目次へ