【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は、プログラムやファイルタイプを識別するための名前で、例えばtxtfile
やjpegfile
などがよく使われます。
この値が空や存在しない場合は、その拡張子に関連付けられたプログラムが設定されていないことを意味します。
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
:ユーザーが選択したProgIDHash
:設定の改ざんを防ぐためのハッシュ値
この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\Software
やHKEY_CLASSES_ROOT
を参照すると、実際にはHKEY_LOCAL_MACHINE\Software\WOW6432Node
やHKEY_CLASSES_ROOT\WOW6432Node
にリダイレクトされることがあります。
このため、C#のアプリケーションが32ビットでビルドされている場合、64ビット用の関連付け情報を正しく取得できないことがあります。
特に、64ビット版の既定アプリ設定はWOW6432Nodeの下には存在しないため、32ビットアプリからは見えないことがあります。
これを回避するには、Microsoft.Win32.RegistryKey
のOpenBaseKey
メソッドと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_ROOT
やHKEY_CURRENT_USER
など)を表す静的プロパティを提供しています。
例えば、Registry.ClassesRoot
やRegistry.CurrentUser
などです。
これらはルートキーを表すRegistryKey
オブジェクトを返します。
一方、RegistryKey
クラスは、レジストリキーの開閉、値の読み書き、サブキーの列挙などの操作を行うためのインスタンスクラスです。
Registry
クラスの静的プロパティやメソッドで取得したRegistryKey
オブジェクトを使って、さらに深い階層のキーを開いたり、値を取得したりします。
使い分けのポイントは以下の通りです。
- ルートキーを取得する際は
Registry
クラスの静的プロパティを使います - ルートキー以下のキーを操作する際は
RegistryKey
のOpenSubKey
やCreateSubKey
を使います - 値の読み書きは
RegistryKey
のGetValue
やSetValue
を使います
例えば、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_USER
のFileExts
キーなども参照する場合は、同様に読み取り権限があれば問題ありません。
逆に、書き込みや変更を行う場合は管理者権限が必要になることが多いので、読み取り専用の用途であれば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
メソッドを使って、キーの既定値を取得します。
具体的な流れは以下の通りです。
RegistryKey.OpenBaseKey
でHKEY_CLASSES_ROOT
を開き、適切なRegistryView
(64ビットまたは32ビット)を指定します。OpenSubKey
で拡張子(例:.txt
)のキーを開きます。GetValue
メソッドに空文字列""
を渡して既定値を取得します。
この既定値がProgIDに該当します。
ProgIDは文字列で、例えばtxtfile
やjpegfile
などの名前が返されます。
以下はサンプルコードです。
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\<拡張子>\UserChoice
のProgId
値に設定があることがあります。
ここを参照してみるのも有効です。
- 拡張子キーの既定値が空でも、
PerceivedType
やContent 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
クラスのIsPathRooted
やUri
クラスを使ってパスの種類を判別し、ネットワークパスの場合はアクセス可能かどうかを事前にチェックする方法があります。
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\<拡張子>\UserChoice
のProgId
値が変わることで、関連付けが切り替わります。
この変更を検知してアプリケーション側で既定アプリの変化に対応するには、レジストリの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.Run
やBackgroundWorker
などで非同期に実行します - 結果をUIに反映する際は、
Invoke
やBeginInvoke
を使って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
キーの変更が制限され、システム管理者が設定した既定アプリが強制されます。
- レジストリの特定キーが読み取り専用または非表示になる
アクセス権限が制限されているため、アプリケーションが関連付け情報を取得しようとしてもUnauthorizedAccessException
やSecurityException
が発生することがあります。
- ポリシーによる関連付けの上書き
グループポリシーで設定された関連付けがUserChoice
よりも優先される場合があり、通常のユーザー設定が反映されないことがあります。
- 管理用ツールやスクリプトでの一括設定
企業環境では、管理者がPowerShellスクリプトや専用ツールで関連付けを一括設定していることが多く、これらの設定がレジストリに反映されます。
このような環境では、アプリケーション側で関連付け情報を取得する際に例外処理を強化し、権限不足や設定の不整合に対応する必要があります。
また、管理者に問い合わせてポリシーの内容を確認することも重要です。
まとめ
この記事では、C#で拡張子に関連付けられた既定アプリをレジストリから取得する方法を解説しました。
HKEY_CLASSES_ROOT
の拡張子キーからProgIDを取得し、ProgIDのshell\open\command
キーから実行コマンドを読み取る流れや、ユーザー設定を反映するUserChoice
キーの扱い、32/64ビット環境の注意点も説明しています。
例外処理やパフォーマンス最適化、既定アプリ変更の監視方法も紹介し、実用的なサンプルコードも掲載しました。
これにより、Windowsの関連付け情報を正確に取得し、柔軟に活用できる知識が身につきます。