気まま研究所ブログ

ITとバイク、思ったことをてきとーに書きます。

C# 高速にフォアグラウンドのプロセス名を取得する

f:id:AonaSuzutsuki:20190828112131p:plain

こんにちは。
最近バイクネタかWindowsネタばかりでしたが、今回はC#ネタを少し。
C#ではProcessクラスよりプロセス名の取得が簡単にできます。
しかしながら、プロセス取得は重い処理なため、取得頻度が多いとボトルネックになることがあります。
そこで今回はWin32APIを用いて高速にプロセス名を取得してみます。
なお、Chromeなどの複数プロセスを連携するアプリの場合はうまく取得できない場合があります。
ビルド設定を32bit優先にしないようにすれば取れます。

Processクラスの問題点

以前紹介したMabinogi Key Converterにてマビノギのプロセスが起動しているかどうかをチェックする機構を組み込んでいます。
今回アップデートで有効化したままマビノギを起動しても検知できるようにキー押下イベントでプロセス情報を取得することにしました。
元々はWin32APIでフォアグラウンドのプロセスIDを取得し、そこから標準ライブラリのProcessクラスを用いて取得していましたが、連続でキー押下すると入力に遅延が発生するんですよね。
なんだと思ったらProcessクラスがボトルネックになってることが見えてきました。

var handle = GetForegroundWindow();
_ = GetWindowThreadProcessId(handle, out var processId);
var process = Process.GetProcessById((int)processId);
Console.WriteLine(process.ProcessName);

Processクラスは簡単にプロセス情報を取得でき、ある程度操作もできるので非常に便利なクラスなのですが、その反面処理が多くなっています。
そのため、単にプロセス名が欲しいだけの場合にはちょっと大げさな部分もあるんですよね。
ちなみにソースコードが公開されているのでProcess#ProcessNameの部分をみてみると結構複雑なことをやっています。
これを見ているとやっぱりと言うべきか、内部ではWin32APIを呼んでいますね。

というわけでWin32APIに全ての希望を託してみようと思ったのが今回の内容です。
ただし、Win32APIはWindows限定になってしまうのでLinuxmacOSで動かすアプリには適用できません。

ちなみに、.Net CoreのProcessはOSごとに対応しているみたい。

検証環境

項目 詳細
OS Windows 10 Pro x64 1903
CPU Intel Core i5 4590
.Net .Net Framework 4.7.1

.Net Coreなどは試してませんのでなんとも言えませんが、.Net Frameworkならだいたい動くと思います。
また、Windows 10以外は確認していないのでWindows 7などの古いOSでは動かないかもしれません。
リファレンスみてたら一番若いのでGetModuleBaseName関数が最低でもWindows Vistaだったので、少なくとも現在サポートされているOSでは動くと思います。

実装

Win32APIメソッドの宣言

#region Win32API Methods
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr handle);

[DllImport("psapi.dll", CharSet = CharSet.Ansi)]
private static extern uint GetModuleBaseName(IntPtr hWnd, IntPtr hModule, [MarshalAs(UnmanagedType.LPStr), Out] StringBuilder lpBaseName, uint nSize);
#endregion

まずはWin32APIのメソッドを宣言します。
これらを駆使することでプロセス名を高速に取得することができます。

プロセス名の取得

var handle = GetForegroundWindow();
_ = GetWindowThreadProcessId(handle, out var processId);

if (handle != IntPtr.Zero)
{
    var hnd = OpenProcess(0x0400 | 0x0010 , false, processId);

    var buffer = new StringBuilder(255);
    GetModuleBaseName(hnd, IntPtr.Zero, buffer, (uint)buffer.Capacity);

    CloseHandle(hnd);

    var processName = buffer.ToString().ToLower();
    Console.WriteLine(processName);
}

これをマビノギにフォーカスを当てた状態で実行すると

client.exe

が取得できます。

詳しい説明は後述します。

また、試してませんが、GetForegroundWindow関数でなくてもウィンドウハンドルが取れれば取得できると思います。

GetForegroundWindow関数

var handle = GetForegroundWindow();

GetForegroundWindow関数はフォーカスの当たっているウィンドウハンドルを取得します
引数は何も指定する必要がなく、単に取得したいアプリにフォーカスが当たっていればハンドルが帰ってきます。
フォーカスが当たっていない場合などの特定の条件下ではNULL(IntPtr.Zero)が返ってきます。

GetWindowThreadProcessId関数

_ = GetWindowThreadProcessId(handle, out var processId);

GetWindowThreadProcessId関数は第一引数に指定されたウィンドウハンドルからスレッドIDとプロセスIDを取得します
戻り値はスレッドIDで、第二引数にはC++上のunsigned longのポインタを渡すことでプロセスIDを取得します。
ここではout修飾子で参照を渡すことでプロセスIDを取得しています。

OpenProcess関数

var hnd = OpenProcess(0x0400 | 0x0010 , false, processId);

OpenProcess関数は第二引数のプロセスIDと一致するプロセスを開きます
第一引数は権限を指定する項目で、今回はプロセス情報を読み取りたいので 0x0400(PROCESS_QUERY_INFORMATION)と0x0010(PROCESS_VM_READ) を指定しています。
なお、細かい権限についてはProcess Security and Access Rightsをご参照ください。

GetModuleBaseName関数

var buffer = new StringBuilder(255);
GetModuleBaseName(hnd, IntPtr.Zero, buffer, (uint)buffer.Capacity);
var processName = buffer.ToString().ToLower();

そして、GetModuleBaseName関数に開いたプロセスのハンドルを渡すことでプロセス名を取得します
第二引数はモジュールを指定するようですが、NULLにすることでプロセス名が得られます
なお、OpenProcess関数でPROCESS_QUERY_INFORMATIONとPROCESS_VM_READ権限を指定していないと取得できません。

最後にStringBuilder#ToStringメソッドで文字列を得られますが、ファイルシステム上のファイル名と一致しないことが多いので統一するために全部小文字にしました。
例えば、マビノギならClient.exeのはずがclient.exeになっていたりするので全て大文字かどっちかに統一しておきましょう。

CloseHandle関数

CloseHandle(hnd);

最後にCloseHandle関数で開けたプロセスハンドルを閉じます。

なお、ウィンドウハンドルは閉じる必要はありません。
閉じることのできるハンドルはリファレンスにも書かれていますが、ウィンドウハンドルはそもそも閉じることができません。

これにより、連続したキー押下にも対応できるほど高速に取得できるようになりました。
速度は測っていませんが、体感できるほどの違いがありますね。
Processの時はキーを押し続けていると0.2秒ほど固まる時間があり、入力が遅延していましたが、Win32APIに変えてからは遅延が一切ありません。

環境依存で手数も増えてめんどうですが、Processの遅延が気になる方は一度試してみる価値ありだと思います。

ちなみに、このコードを使ったアプリは特定のキーを別のキーに変換するソフト 「MabinogiKeyConverter」で紹介しているのでぜひ使ってみてください。