FindWindow vs EnumWindows: Finding Windows in Win32

Find another program's window with FindWindow: class name vs title, partial matches via EnumWindows, the ANSI build trap, UIPI, and a C# LibraryImport version.

Revised November 2026. This tutorial first appeared in Korean in 2021. The original called a window handle "an integer" and printed it with printf("%d", hwnd), which GCC flags with -Wformat; it also described the second parameter of FindWindowEx incorrectly. Its example target, Minesweeper, has not shipped with Windows since Windows 8, so that example and its screenshot are gone. All C++ code here was compiled for Windows with MinGW-w64 (GCC 16.2.0); none of it was run on this machine, which is a Mac. The C# version was built with the .NET 10 SDK, also without running it. We show no invented output; behavior is described from the Microsoft Learn documentation.

This article is for developers who need to check whether another program's window is open, or get the handle of a button inside it for automation. The short version: if you know the class name, call FindWindowW(className, NULL); if the title is fixed, FindWindowW(NULL, title); if you only know part of the title, enumerate with EnumWindows and compare yourself. What you can then do with the handle depends on the other process's privilege level — UIPI.

What FindWindow and FindWindowEx really do

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

Some of the 2021 text holds up: NULL for either argument means "any", the comparison is case-insensitive, and there is no partial matching. On success you get an HWND; on failure, NULL. (The old text spelled the first parameter IpClassName; it is lpClassName, with a lowercase L for "long pointer".)

Three points needed correcting:

  • FindWindow searches top-level windows only. Child windows such as buttons and edit boxes are never matched.
  • The second parameter of FindWindowEx, hWndChildAfter, is not "the child window's handle". It is where the search starts: the search begins with the child that follows it in Z order, and NULL starts at the first child. Passing the previous result back in lets you walk through several buttons of the same class. FindWindowEx also searches direct children only, never grandchildren.
  • hWndParent == NULL means "children of the desktop", i.e. top-level windows. The old sample called FindWindowEx(h1, ...) even when h1 was NULL, so with the form closed it went looking for a top-level window titled "button". Check the parent before searching inside it.

A handle is also not just "a number the OS assigns". HWND is an opaque, pointer-sized value — 8 bytes in a 64-bit process. The documentation does guarantee that only the lower 32 bits are significant so that 32- and 64-bit processes can exchange handles, but %d is still the wrong format.

Compiling the 2021 code with MinGW

We compiled both original samples with x86_64-w64-mingw32-g++ -Wall. The first one (checking whether Minesweeper is running) compiled cleanly. The second one (printing the form and button handles) produced:

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=]

Passing (void*)h1 to %p silences it. We dropped the handle values and the "Press any key to continue" line from the old output: handle values differ on every run, and that last line comes from Visual Studio's console host, not from the program.

The bigger bug hid in the sample that compiled without warnings. Look at the import table:

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

MinGW does not define UNICODE, so FindWindow became the ANSI function FindWindowA. GCC, meanwhile, stores string literals as UTF-8. Searching the exe for the Korean title from the original sample, we found its UTF-8 bytes (ec a7 80 …, 13 bytes) and not its code page 949 bytes (c1 f6 b7 da …, 9 bytes). FindWindowA interprets the bytes it receives in the system's ANSI code page, so on a Korean system it searches for a different string and reports "not running" even while the window is open. The same applies to any non-ASCII title — accented Latin characters on code page 1252 included. In 2021 the project was built by Visual Studio as Unicode, which hid the problem.

We confirmed that -fexec-charset=CP949 puts the CP949 bytes into the exe, but that only moves the breakage to PCs with another code page. The real fix is FindWindowW with an L"..." literal; with -DUNICODE the import switched to FindWindowW and the exe contained the UTF-16 string.

Search by class name; use EnumWindows for partial matches

Titles change more often than you think. Notepad's title includes the file name, a different display language translates it, and a browser's title follows the active tab. The class name is the more stable key. You can read it with Spy++ (included with Visual Studio) or Inspect from the Windows SDK. Classic Notepad registered the class Notepad; apps get rewritten (Windows 11 shipped a new Notepad), so check the class on the version you target.

Decision diagram. If you know the window class name, call FindWindowW with the class and NULL; from there FindWindowExW finds direct child controls. If not, but the title is exact and fixed, call FindWindowW with NULL and the title. If the title is only partly known or changes, enumerate all top-level windows with EnumWindows and compare GetWindowTextW yourself.
Choosing a search method. A class name is the most stable key; titles change with the language and the open document, and only EnumWindows can match part of one.

For a partial title match, FindWindow is useless: enumerate every top-level window with EnumWindows and compare. The program below shows all three techniques:

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

// case-insensitive "contains" for window titles
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;           // skip hidden helpers
    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 stops the enumeration
}

int wmain(int argc, wchar_t** argv)
{
    // 1) exact match by class name: stable across languages and documents
    HWND byClass = FindWindowW(L"Notepad", nullptr);
    wprintf(L"FindWindowW(L\"Notepad\", NULL) -> %p\n", (void*)byClass);

    // 2) all direct children of that window, one after another
    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) partial, case-insensitive title match: FindWindow cannot do this
    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

Zero warnings, and every import is a W function. We did not run it, so there is no output to show. Things to keep in mind:

  • EnumWindows hands you invisible top-level windows too, and programs create plenty of hidden helper windows, so filtering with IsWindowVisible is the norm.
  • Returning FALSE from the callback stops the enumeration — useful when one match is enough.
  • GetWindowTextW reads the caption of a top-level window in another process, but according to Microsoft Learn it cannot read the text of a control in another process. For that you must send WM_GETTEXT yourself, and then UIPI (next section) applies.
  • To match "Notepad" as well as "notepad", the code lowercases both strings with CharLowerBuffW; FindWindow does case-insensitive comparison for you, but only on the whole string.

UIPI: you can find it, but you may not touch it

User Interface Privilege Isolation, introduced in Windows Vista, stops a process with a lower integrity level from driving the windows of a higher one. Normal programs run at medium integrity; programs started "as administrator" run at high integrity.

What matters is what is blocked and what is not. FindWindow, EnumWindows and GetWindowText on a top-level caption work across integrity levels, so you can get the handle of an elevated program's window. Sending it most messages with SendMessage or PostMessage, faking input with SendInput, or hooking it, is blocked. The SendMessage documentation on Microsoft Learn says that when UIPI blocks a message, GetLastError returns 5 (access denied).

So when "FindWindow returns a handle but the button click does nothing", first check whether the target runs elevated. The fix is to run your tool at the same level; if you wrote both programs, the receiver can let specific messages in with ChangeWindowMessageFilterEx. That is continued, with a diagram, in SendMessage vs PostMessage.

The same search from C# with LibraryImport

On .NET 10 with LibraryImport (net10.0-windows, AllowUnsafeBlocks on):

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 stops the enumeration
}, 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 built it without warnings into a PE32+ executable (console) x86-64. On the way there we hit these real diagnostics:

  • The old-style [DllImport("user32.dll", CharSet = CharSet.Unicode)] declaration, with SYSLIB1054 raised to a warning in .editorconfig, gives warning SYSLIB1054: Mark the method 'FindWindow' with 'LibraryImportAttribute' instead of 'DllImportAttribute' …. At its default "info" severity it does not appear in build output.
  • Leaving out StringMarshalling gives, for each string parameter, error SYSLIB1051: Marshalling string or char without explicit marshalling information is not supported. LibraryImport never silently picks ANSI the way DllImport does by default, so the ANSI trap above becomes a compile error in C#.
  • Our first buffer parameter was [Out] char[], which failed with error SYSLIB1051: Runtime marshalling must be disabled in this project … to enable marshalling this type. Changing it to Span<char> fixed it; an array converts to Span<char> implicitly.

The lambda passed to EnumWindows is marshalled as a delegate. In the generated source we could see Marshal.GetFunctionPointerForDelegate before the call and GC.KeepAlive after it. That is enough for an API that uses the callback only during the call, like EnumWindows. A callback that is called later — a window procedure, for example — needs its lifetime managed by you; CreateWindowEx and ShowWindow shows that case.

Which method, when

Situation Use
Your own window, or any window whose class you know FindWindowW(L"Class", nullptr)
A fixed, exactly known title FindWindowW(nullptr, L"Title")
Part of a title, any case, several hits EnumWindows + GetWindowTextW
Buttons and edit boxes inside a window Repeat FindWindowExW(parent, previous, L"Button", …); for deeper trees, EnumChildWindows
Preventing a second instance of your app A named mutex (CreateMutexW) is more reliable than FindWindow
Controlling an elevated program Run your tool elevated too (UIPI)

Sending a click or text to the handle you found continues in SendMessage vs PostMessage.

Tested with MinGW-w64 x86_64-w64-mingw32-g++ (GCC) 16.2.0 and .NET SDK 10.0.403 (EnableWindowsTargeting=true) on macOS (Apple Silicon). Compiled for Windows; not run on this machine.

FAQ

Can FindWindow match part of a window title?

No. FindWindow ignores case, but the whole title must match. To find every window whose title contains a word, enumerate top-level windows with EnumWindows, read each title with GetWindowTextW and compare it yourself. Where possible, search by class name instead, because it does not change with the UI language or the open document.

FindWindow always returns NULL for a title with non-ASCII characters. Why?

Probably an ANSI build that calls FindWindowA. GCC stores string literals as UTF-8, while FindWindowA interprets the bytes in the system ANSI code page, so the title never matches. Use FindWindowW with an L"..." literal.

Can I find a window that belongs to a program running as administrator?

Yes. FindWindow and EnumWindows return handles regardless of integrity level. What UIPI blocks is acting on that window: most messages sent with SendMessage or PostMessage from a normal process are dropped. To control it, run your program at the same integrity level.

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

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑