FindWindowでウィンドウハンドルを探す:クラス名・部分一致・UIPI

FindWindow・FindWindowExで他のプログラムのウィンドウハンドルを探す方法。クラス名とタイトルの違い、EnumWindowsでの部分一致、ANSIビルドの文字化けの罠、UIPI、C# LibraryImport版まで。

2026年11月改訂: この記事は2021年に韓国語で書いた FindWindow の講座を、日本語向けに書き直したものです。元の記事はハンドルを「整数値」と説明して printf("%d", hwnd) で表示していましたが、このコードは GCC で -Wformat の警告が出ました。FindWindowEx の第2引数の説明も誤っていました(下で直します)。例に使った「マインスイーパー」は Windows 8 から標準で入っていないため、スクリーンショットごと外しました。C++ のコードはすべて MinGW-w64(GCC 16.2.0)で Windows 向けにコンパイルしましたが、執筆に使った Mac では実行していません。C# 版も .NET 10 SDK でビルドしただけです。実行結果は作らず、動作は Microsoft Learn のドキュメントに基づいて説明します。

ほかのプログラムのウィンドウが開いているか確かめたい、その中のボタンのハンドルを取って自動化したい、という人向けの記事です。先に結論を書くと、クラス名がわかるなら FindWindowW(クラス名, NULL)、タイトルが固定なら FindWindowW(NULL, タイトル)、タイトルの一部しかわからないなら EnumWindows で自分で比べるしかありません。そして見つけたハンドルで何ができるかは、相手のプロセスの権限(UIPI)で決まります。

FindWindow と FindWindowEx の正確な動き

HWND FindWindowW(LPCWSTR lpClassName, LPCWSTR lpWindowName);
HWND FindWindowExW(HWND hWndParent, HWND hWndChildAfter,
                   LPCWSTR lpszClass, LPCWSTR lpszWindow);

元の記事で正しかった点から確認すると、どちらの引数も NULL なら「何でもよい」、比較は大文字小文字を区別せず、部分一致はしません。見つかればウィンドウハンドル(HWND)、見つからなければ NULL を返します。第1引数の名前は IpClassName ではなく、小文字の L で始まる lpClassName(long pointer)です。

直すべき点は3つあります。

  • FindWindow が探すのはトップレベルウィンドウだけです。 ボタンやエディットのような子ウィンドウは対象外です。
  • FindWindowEx の第2引数 hWndChildAfter は「子ウィンドウのハンドル」ではなく、「この子の次から探す」という開始位置です。 Z オーダーでその次の子から検索が始まり、NULL なら最初の子からです。同じクラスのボタンが複数あるとき、前回の結果を渡し直せば順に全部たどれます。また FindWindowEx は直下の子だけを探し、孫までは降りません。
  • hWndParent が NULL だとデスクトップの子、つまりトップレベルウィンドウを探します。 元の例はフォームが見つからなかったときも FindWindowEx(h1, ...) を呼んでいたので、フォームがなければ「ボタン」というタイトルのトップレベルウィンドウを探しに行きます。親が見つかったかを先に確認すべきです。

ハンドルも「OS が付けた整数」とだけ覚えると危険です。HWND はポインターサイズの不透明な値で、64 ビットプロセスでは8バイトです。32 ビットと 64 ビットのプロセスがハンドルをやり取りできるよう下位32ビットだけが意味を持つとドキュメントにはありますが、それでも %d は書式として誤りです。

2021年のコードを MinGW でコンパイルすると

元の2つの例を x86_64-w64-mingw32-g++ -Wall でコンパイルしました。1つ目(マインスイーパーが動いているかの確認)は警告なしで通り、2つ目(フォームとボタンのハンドル表示)は次の警告を出しました。

find2021b.cpp:10:31: warning: format '%d' expects argument of type 'int', but argument 2 has type 'HWND' {aka 'HWND__*'} [-Wformat=]
   10 |         printf("폼의 핸들값: %d\n버튼의 핸들값: %d\n", h1, h2);
find2021b.cpp:10:50: warning: format '%d' expects argument of type 'int', but argument 3 has type 'HWND' {aka 'HWND__*'} [-Wformat=]

%p に (void*)h1 を渡せば警告は消えます。元の記事に載せていたハンドル値と「続行するには何かキーを押してください」の行は外しました。ハンドル値は実行するたびに変わり、最後の行はプログラムではなく Visual Studio のコンソールが付け足す文だからです。

もっと大きな問題は、警告の出なかった1つ目に隠れていました。exe のインポートテーブルを見るとこうなっています。

$ x86_64-w64-mingw32-objdump -p find2021a.exe | grep FindWindow
    00008420  <none>  010f  FindWindowA

MinGW は UNICODE を定義しないので、FindWindow は ANSI 版の FindWindowA になりました。一方 GCC は文字列リテラルを UTF-8 で格納します。exe の中から元の例の韓国語タイトルを探すと、UTF-8 のバイト列(13バイト)はあり、韓国語の ANSI コードページ CP949 のバイト列(9バイト)はありませんでした。FindWindowA は受け取ったバイト列をシステムの ANSI コードページで解釈するため、ウィンドウが開いていても「起動していません」と出ることになります。日本語でも事情は同じで、たとえば「無題 - メモ帳」は UTF-8 では18バイト、Shift_JIS(CP932)では13バイトと、まったく別のバイト列です。2021年当時は Visual Studio が Unicode でビルドしていたので表に出なかった問題です。

-fexec-charset=CP949 を付けると exe に CP949 のバイト列が入ることも確かめましたが、コードページの違う PC では再び壊れます。正解は FindWindowW と L"…" の文字列です。-DUNICODE でコンパイルするとインポートが FindWindowW に変わり、exe には UTF-16 の文字列が入りました。

クラス名で探す、部分一致は EnumWindows

タイトルは思ったより変わります。メモ帳のタイトルは開いているファイル名で変わり、表示言語を英語にすれば「Notepad」になり、ブラウザーはタブごとに違います。だからクラス名のほうが安定したキーです。クラス名は Visual Studio 付属の Spy++ や Windows SDK の Inspect で確認できます。従来のメモ帳は Notepad クラスを使っていましたが、Windows 11 で新しいメモ帳に置き換わったように、アプリが変われば構造も変わりうるので、対象のバージョンで確かめてください。

判断図。クラス名がわかるなら FindWindowW(クラス名, NULL)、そこから子コントロールは FindWindowExW で直下の子を探す。わからなくても、タイトルが固定で完全一致するなら FindWindowW(NULL, タイトル)。タイトルの一部しかわからない、または変わるなら、EnumWindows で全トップレベルウィンドウを列挙して GetWindowTextW の結果を自分で比べる。
検索方法の選び方。いちばん安定したキーはクラス名。タイトルは表示言語や開いている文書で変わり、部分一致できるのは EnumWindows だけ。

タイトルの一部で探すなら FindWindow では無理で、EnumWindows で全トップレベルウィンドウを受け取って比べます。次のコードは3つの方法をまとめて示します。

// find.cpp — FindWindowW / FindWindowExW / EnumWindows(Unicode)
#include <windows.h>
#include <cstdio>
#include <cwchar>
#include <string>

// 大文字小文字を無視した「含む」比較
static bool ContainsNoCase(const wchar_t* text, const wchar_t* part)
{
    std::wstring t(text), p(part);
    CharLowerBuffW(t.data(), static_cast<DWORD>(t.size()));
    CharLowerBuffW(p.data(), static_cast<DWORD>(p.size()));
    return t.find(p) != std::wstring::npos;
}

struct Search { const wchar_t* part; int hits; };

static BOOL CALLBACK OnWindow(HWND hwnd, LPARAM lp)
{
    auto* s = reinterpret_cast<Search*>(lp);
    if (!IsWindowVisible(hwnd)) return TRUE;           // 非表示の補助ウィンドウは飛ばす
    wchar_t title[256], cls[256];
    if (GetWindowTextW(hwnd, title, 256) == 0) return TRUE;
    GetClassNameW(hwnd, cls, 256);
    if (ContainsNoCase(title, s->part)) {
        DWORD pid = 0;
        GetWindowThreadProcessId(hwnd, &pid);
        wprintf(L"%p  pid=%lu  class=%ls  title=%ls\n", (void*)hwnd, pid, cls, title);
        s->hits++;
    }
    return TRUE;                                       // FALSE で列挙を止める
}

int wmain(int argc, wchar_t** argv)
{
    // 1) クラス名で完全一致
    HWND byClass = FindWindowW(L"Notepad", nullptr);
    wprintf(L"FindWindowW(L\"Notepad\", NULL) -> %p\n", (void*)byClass);

    // 2) そのウィンドウの直下の子を順番に
    for (HWND child = nullptr;
         byClass && (child = FindWindowExW(byClass, child, nullptr, nullptr)); ) {
        wchar_t cls[256];
        GetClassNameW(child, cls, 256);
        wprintf(L"  child %p  class=%ls\n", (void*)child, cls);
    }

    // 3) タイトルの部分一致(大文字小文字無視):FindWindow ではできない
    Search s{ argc > 1 ? argv[1] : L"notepad", 0 };
    EnumWindows(OnWindow, reinterpret_cast<LPARAM>(&s));
    wprintf(L"%d window(s) contain \"%ls\"\n", s.hits, s.part);
    return 0;
}
$ x86_64-w64-mingw32-g++ -std=c++17 -Wall -Wextra -O2 -municode find.cpp -o find.exe -luser32
$ file find.exe
find.exe: PE32+ executable (console) x86-64, for MS Windows
$ x86_64-w64-mingw32-objdump -p find.exe | grep -E "FindWindow|EnumWindows|GetWindowText"
    0000e600  <none>  0108  EnumWindows
    0000e608  <none>  0111  FindWindowExW
    0000e610  <none>  0112  FindWindowW
    0000e620  <none>  01ee  GetWindowTextW

警告は0件、インポートはすべて W 版です。実行はしていないので出力は載せません。使うときの注意点です。

  • EnumWindows は非表示のトップレベルウィンドウもすべて渡してきます。プログラムが内部用に作る隠しウィンドウは多いので、IsWindowVisible でふるい落とすのが普通です。
  • コールバックで FALSE を返すと列挙が止まります。1件見つかれば十分なときに使えます。
  • GetWindowTextW は他プロセスのトップレベルウィンドウのタイトルは読めますが、Microsoft Learn によると他プロセスのコントロールのテキストは読めません。その場合は WM_GETTEXT を自分で送る必要があり、次の節の UIPI の制約を受けます。
  • コンソールに日本語を wprintf で出すと、コンソールのコードページ次第で化けることがあります。結果はファイルやログに残すほうが確実です。

UIPI:見つけられても操作はできない

Windows Vista から入った UIPI(User Interface Privilege Isolation、ユーザーインターフェイス特権の分離)は、整合性レベルの低いプロセスが高いプロセスのウィンドウを操作するのを防ぎます。普通のプログラムは「中」、「管理者として実行」したプログラムは「高」です。

大事なのは何が止められ、何が止められないかです。FindWindow、EnumWindows、トップレベルのタイトルを読む GetWindowText は整合性レベルに関係なく動くので、管理者権限のプログラムのウィンドウでもハンドルまでは取れます。しかしそのハンドルに SendMessage/PostMessage でほとんどのメッセージを送る、SendInput で入力をまねる、フックする、といったことは止められます。Microsoft Learn の SendMessage のドキュメントによれば、UIPI に止められたとき GetLastError は5(アクセス拒否)です。

「FindWindow でハンドルは取れたのにボタンのクリックが効かない」ときは、まず相手が管理者権限で動いていないかを確認してください。対処は自分のツールも同じ権限で実行することです。両方とも自分で作っているなら、受信側で ChangeWindowMessageFilterEx を使い必要なメッセージだけを通す手もあります。図を使った説明は SendMessage と PostMessage に続きます。

C# の LibraryImport で探す

.NET 10 の LibraryImport で書くと次のとおりです(net10.0-windows、AllowUnsafeBlocks を有効化)。

using System.Runtime.InteropServices;

string part = args.Length > 0 ? args[0] : "notepad";

nint byClass = Native.FindWindowW("Notepad", null);
Console.WriteLine($"FindWindowW(\"Notepad\", null) -> 0x{byClass:X}");

var hits = new List<(nint Hwnd, uint Pid, string Title)>();
Native.EnumWindows((hwnd, _) =>
{
    if (!Native.IsWindowVisible(hwnd)) return true;
    int len = Native.GetWindowTextLengthW(hwnd);
    if (len == 0) return true;
    var buf = new char[len + 1];
    int n = Native.GetWindowTextW(hwnd, buf, buf.Length);
    var title = new string(buf, 0, n);
    if (title.Contains(part, StringComparison.OrdinalIgnoreCase))
    {
        Native.GetWindowThreadProcessId(hwnd, out uint pid);
        hits.Add((hwnd, pid, title));
    }
    return true;                       // false で列挙を止める
}, 0);

foreach (var (h, pid, title) in hits)
    Console.WriteLine($"0x{h:X}  pid={pid}  {title}");

static partial class Native
{
    public delegate bool EnumWindowsProc(nint hwnd, nint lParam);

    [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
    public static partial nint FindWindowW(string? lpClassName, string? lpWindowName);

    [LibraryImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static partial bool EnumWindows(EnumWindowsProc callback, nint lParam);

    [LibraryImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static partial bool IsWindowVisible(nint hwnd);

    [LibraryImport("user32.dll")]
    public static partial int GetWindowTextLengthW(nint hwnd);

    [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
    public static partial int GetWindowTextW(nint hwnd, Span<char> text, int maxCount);

    [LibraryImport("user32.dll")]
    public static partial uint GetWindowThreadProcessId(nint hwnd, out uint processId);
}

dotnet build -c Release -r win-x64 -p:EnableWindowsTargeting=true で警告なしにビルドでき、成果物は PE32+ executable (console) x86-64 でした。ここまでに実際に出た診断は次のとおりです。

  • 従来の [DllImport("user32.dll", CharSet = CharSet.Unicode)] で宣言し、.editorconfig で SYSLIB1054 を警告に上げると warning SYSLIB1054: Mark the method 'FindWindow' with 'LibraryImportAttribute' instead of 'DllImportAttribute' … が出ます。既定の重大度は「情報」なのでビルド出力には出ません。
  • StringMarshalling を省くと、string 引数ごとに error SYSLIB1051: Marshalling string or char without explicit marshalling information is not supported になります。DllImport の既定のように黙って ANSI を選ぶことがないので、上で見た ANSI の罠は C# ではコンパイルエラーとして先に止まります。
  • 最初はバッファーを [Out] char[] で宣言していて、error SYSLIB1051: Runtime marshalling must be disabled in this project … to enable marshalling this type になりました。Span<char> に変えると通りました(配列は Span<char> に暗黙変換されます)。

EnumWindows に渡したラムダはデリゲートとしてマーシャリングされます。生成されたコードを開くと、呼び出し前に Marshal.GetFunctionPointerForDelegate で関数ポインターを作り、呼び出し後に GC.KeepAlive でデリゲートを保持していました。EnumWindows のように呼び出し中だけコールバックを使う API ならこれで足りますが、あとから呼ばれるコールバック(ウィンドウプロシージャなど)は寿命を自分で管理する必要があります。その例は CreateWindow・ShowWindow にあります。

まとめ:状況別の選び方

状況 方法
自作のウィンドウ、またはクラス名がわかるウィンドウ FindWindowW(L"クラス名", nullptr)
タイトルが固定で正確にわかる FindWindowW(nullptr, L"タイトル")
タイトルの一部、大文字小文字無視、複数件 EnumWindows + GetWindowTextW
ウィンドウ内のボタンやエディット FindWindowExW(親, 前の子, L"Button", …) を繰り返す。深い階層は EnumChildWindows
アプリの二重起動防止 FindWindow より名前付きミューテックス(CreateMutexW)が確実
管理者権限のプログラムを操作 自分のツールも同じ権限で実行(UIPI)

見つけたハンドルにクリックやテキストを送る方法は SendMessage と PostMessage に続きます。

検証環境:macOS(Apple Silicon)上の MinGW-w64 x86_64-w64-mingw32-g++ (GCC) 16.2.0、.NET SDK 10.0.403(EnableWindowsTargeting=true)。Windows 向けにコンパイルのみ行い、実行はしていません。

よくある質問

FindWindow でタイトルの一部だけで探せますか?

できません。FindWindow は大文字小文字を区別しないだけで、タイトル全体が一致する必要があります。「メモ帳」を含むウィンドウを探すなら、EnumWindows でトップレベルウィンドウを1つずつ受け取り、GetWindowTextW で読んだタイトルを自分で比べます。できれば表示言語や開いている文書で変わらないクラス名で探すほうが安定します。

日本語のタイトルを渡すと FindWindow がいつも NULL を返します。

ANSI ビルドで FindWindowA が呼ばれている可能性が高いです。MinGW(GCC)は文字列リテラルを UTF-8 で格納しますが、FindWindowA は受け取ったバイト列をシステムの ANSI コードページ(日本語版 Windows は CP932)として解釈するので一致しません。FindWindowW と L"…" の文字列を使えば解決します。

管理者として実行されたプログラムのウィンドウも探せますか?

探すことはできます。FindWindow と EnumWindows は整合性レベルに関係なくハンドルを返します。ただしそのウィンドウへ SendMessage や PostMessage でほとんどのメッセージを送ることは UIPI が止めます。操作まで必要なら、自分のプログラムも同じ権限で実行します。

  • #Win32
  • #FindWindow
  • #FindWindowEx
  • #EnumWindows
  • #UIPI
  • #LibraryImport

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑