2020年8月1日土曜日

【Windows】手前ウィンドウのタイトルとフルパスを取得するツール【プロセス】


概要

<手前ウィンドウのタイトル>と、<手前ウィンドウを表示するプロセスのモジュールのフルパス>をコンソール出力するツールを作成した。
動作の様子。500ミリ秒おきにコンソール出力する。


製造

百聞は一見に如かず。
ソースコードの主要な個所を以下に示す。

メイン関数
  1. #include <fcntl.h>
  2. #include <io.h>
  3. #include <stdlib.h>
  4. #include <wchar.h>
  5. #include <windows.h>
  6. #include "process_tools.h"
  7.  
  8. int main(void) {
  9. HWND hWnd = NULL;
  10. DWORD processId = 0;
  11. HANDLE hProcess = NULL;
  12. std::vector<std::wstring> moduleNames;
  13. wchar_t title[MAX_PATH] = {0};
  14.  
  15. // Use Unicode.
  16. _setmode(_fileno(stdout), _O_U8TEXT);
  17.  
  18. while (true) {
  19. // Open process handle.
  20. // Window handle -> Process ID -> Process handle
  21. hWnd = GetForegroundWindow();
  22. GetWindowThreadProcessId(hWnd, &processId);
  23. hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
  24. processId);
  25. if (hProcess == NULL) {
  26. return false;
  27. }
  28.  
  29. // Get name of each module.
  30. tools::EnumModuleNames(hProcess, moduleNames);
  31.  
  32. // Get title of window.
  33. GetWindowText(hWnd, title, MAX_PATH);
  34.  
  35. // Show window title and module names.
  36. wprintf(L"%ls\n", title);
  37. wprintf(L"%u modules\n", moduleNames.size());
  38. for (auto& module : moduleNames) {
  39. wprintf(L" %ls\n", module.c_str());
  40. }
  41. wprintf(L"--------------------------------------------------\n");
  42. wprintf(L"\n");
  43.  
  44. // Close process handle.
  45. CloseHandle(hProcess);
  46.  
  47. // Interval in read.
  48. Sleep(500);
  49. }
  50.  
  51. return EXIT_SUCCESS;
  52. }

ポイント
  • EXE と DLL
    アプリケーションの EXE だけでなく、アプリケーションがロードした DLL を
    合わせてコンソールに表示する。EnumProcessModulesEx 関数がフルパスの一覧を
    取得する。
  • Unicode 出力について
    ウィンドウテキストが日本語の場合もコンソール出力で困らないように
    以下のステップで標準出力を UTF-8 にする。
    1. setmode(_fileno(stdout), _O_U8TEXT);

    ※このステップを入れずにコードページを 65001 とし、
     フォントを日本語対応フォント(MS Gothic)とした際は、
     最初の1文字以降が尻切れになる問題が発生した。

     原因を調べていたところ、参考文献4にこの方法を見つけ、
     問題は解決した。

関数ヘッダファイル
  1. #pragma once
  2.  
  3. #include <wchar.h>
  4. #include <windows.h>
  5. #include <string>
  6. #include <vector>
  7.  
  8. namespace tools {
  9.  
  10. constexpr int ARGUMENT_ERROR = -1;
  11.  
  12. // (中略)
  13.  
  14. int EnumModuleNames(HANDLE hProcess, std::vector<std::wstring>& moduleNames);
  15.  
  16. } // namespace tools

関数ソースファイル
  1. #include "./process_tools.h"
  2. #include <psapi.h>
  3. #include <windows.h>
  4. #include <algorithm>
  5.  
  6. // (中略)
  7.  
  8. namespace tools {
  9.  
  10. // (中略)
  11.  
  12. //
  13. // @brief <br> Get the module file path of given window.
  14. // @param [in] hProcess process handle.
  15. // @param [out] moduleNames Full path for modules.
  16. // @return Actually read module number.
  17. //
  18. int EnumModuleNames(HANDLE hProcess, std::vector<std::wstring>& moduleNames) {
  19. if (hProcess == NULL) {
  20. return ARGUMENT_ERROR;
  21. }
  22.  
  23. // Enumerate modules of process.
  24. DWORD dwSize = 0;
  25. EnumProcessModulesEx(hProcess, NULL, 0, &dwSize, LIST_MODULES_ALL);
  26. std::vector hModules(dwSize / sizeof(HMODULE));
  27. if (EnumProcessModulesEx(
  28. hProcess, hModules.data(),
  29. static_cast(sizeof(HMODULE) * hModules.size()), &dwSize,
  30. LIST_MODULES_ALL) == 0) {
  31. return 0;
  32. }
  33. // Get file path of modules.
  34. moduleNames.clear();
  35. wchar_t strBuf[MAX_PATH] = {0};
  36. for (auto& hModule : hModules) {
  37. GetModuleFileNameEx(hProcess, hModule, strBuf, MAX_PATH);
  38. moduleNames.push_back(strBuf);
  39. }
  40. return static_cast<int>(moduleNames.size());
  41. }
  42. } // namespace tools

ポイント
  • EnumProcessModules を 2回呼び出す
    1回目の呼び出しではプロセスのモジュールの数を取得し、
    2回目の呼び出しではプロセスのモジュールのハンドルを取得する。

    この方法のメリットは、先に数が分かり、動的に配列を準備できることだ。
    この方法のデメリットは、2回の呼び出しに冗長であることと、
    「1回目と2回目の呼び出しの間に、モジュールの数が変わらないの?」
    という懸念があることだ。

    ※尚、MSDN では決め打ちで十分に大きな固定長配列を準備することが
     推奨されている。
  • 処理の流れ
    処理の流れを以下に示す(参考文献1、参考文献2)。

    1. 手前ウィンドウのウィンドウハンドルを取得する(main関数)
    2. ウィンドウハンドルからプロセスIDを取得する(main関数)
    3. プロセスIDのプロセスを開く(main関数)
    4. プロセスのモジュールハンドルを列挙する(本関数)
    5. モジュールのフルパスを取得する(本関数)

    ※EnumProcessModulesEx について、詳しくは MSDN の
     リファレンスを見てほしい(参考文献3)

テスト

googletest を用いて EnumModuleNames 関数の単体テスト(C0)を実施した。
NuGet を利用し gmock v1.10.0 パッケージマネージャをインストールした。
(main関数の方は適当やで)

環境

開発環境および動作確認を実施したPCの情報を以下に示す。


開発環境
Visual Studio Community 2015

PC情報
OS 名: Microsoft Windows 8.1
OS バージョン: 6.3.9600 N/A ビルド 9600
システム モデル: dynabook KIRA V73/PS
システムの種類: x64-based PC
物理メモリの合計: 8,103 MB
プロセッサ: Intel64 Family 6 Model 61 Stepping 4 GenuineIntel ~2200 Mhz

なぜ作った

必要を感じたから。

  • 見切れているウィンドウタイトルの全体を知りたい
  • 長いウィンドウタイトルをコピーしたい

稀によくあるシチュエーションである。

感想

単体テストしないと落ち着かない性質になってしまった。
バグを出さないことは重要であり、そのためには多大な労力が必要なんだな
と感じる今日この頃である。

参考文献

  1. ウィンドウハンドルから実行ファイル名を取得する
  2. DLL / モジュールの列挙 (PSAPI)
  3. EnumProcessModulesEx function
  4. Windowsのwprintf関数はUnicodeを出力できない?
  5. Google C++ Testing Frameworkをはじめよう

0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています