SetWindowText/GetWindowText:バッファ・Unicode・他プロセス

GetWindowTextLength でのバッファ確保、古いサンプルの sizeof バグ、他プロセスのコントロールは WM_GETTEXT で読む理由、C# の LibraryImport 宣言まで。

2026年11月改訂: この記事は2021年に韓国語で公開したものを書き直しました。C++ のサンプルは MinGW-w64 14.0.0(GCC 16.2.0)で Windows 向けにコンパイルしましたが、この記事を書いた Mac では実行していません。 そのため実行画面は載せず、コンパイラが実際に出したエラーと、Microsoft Learn のドキュメントに書かれた動作だけを記しています。2021年版のサンプルには、Unicode ビルドでバッファがあふれうる sizeof(buffer) の誤りと、文字セットの設定次第でコンパイルが通らない文字列リテラルがありました。

この記事は、Win32 でウィンドウのタイトルやコントロールの文字列を書き換えたり読み取ったりしたい方向けです。結論から言うと、自分のウィンドウなら GetWindowTextLengthW で長さを聞き、+1 文字分の wchar_t バッファで GetWindowTextW を呼べば十分です。他のアプリのコントロールは GetWindowText では読めない仕様なので、WM_GETTEXT を自分で送ります。

関数のプロトタイプと戻り値

BOOL WINAPI SetWindowTextW(HWND hWnd, LPCWSTR lpString);
int  WINAPI GetWindowTextW(HWND hWnd, LPWSTR lpString, int nMaxCount);
int  WINAPI GetWindowTextLengthW(HWND hWnd);

接尾辞のない SetWindowText・GetWindowText はマクロで、UNICODE が定義されていれば W 版(UTF-16)、なければ A 版(ANSI コードページ)に展開されます。新しく書くコードなら最初から W 版を直接呼ぶほうが混乱がありません。

  • SetWindowTextW は成功で 0 以外、失敗で 0 を返します。理由は GetLastError で確認します。
  • GetWindowTextW はコピーした文字数(NUL を除く)を返します。nMaxCount は NUL を含むバッファの文字数で、長い文字列は切り詰められます。
  • 2021年版は「失敗すると 0」とだけ書いていましたが、タイトルが空でも 0 です。区別するには呼び出し前に SetLastError(0) し、0 が返ったら GetLastError() を見ます。

2021年のサンプルを再コンパイルした結果

2021年版のコンソール用サンプルは、TCHAR のバッファに普通の文字列 "notepad" を混ぜていました。マルチバイト(ANSI)設定では問題なく通りましたが、UNICODE を定義すると次のようになりました。

$ x86_64-w64-mingw32-g++ -Wall -Wextra -DUNICODE -D_UNICODE orig_console.cpp -luser32
orig_console.cpp:7:28: error: cannot convert 'const char*' to 'LPCWSTR' {aka 'const wchar_t*'}
orig_console.cpp:10:25: warning: format '%s' expects argument of type 'char*', but argument 2 has type 'TCHAR*' {aka 'wchar_t*'} [-Wformat=]
orig_console.cpp:11:25: error: cannot convert 'const char*' to 'LPCWSTR' {aka 'const wchar_t*'}

同じ記事のウィンドウプロシージャ版は L"notepad" を使っていたので逆になり、Unicode ビルドでは通って ANSI ビルドでは cannot convert 'const wchar_t*' to 'LPCSTR' が2件出ました。2つのサンプルが別々のプロジェクト設定を前提にしていたわけです。Visual Studio の新規プロジェクトは Unicode が既定なので、コンソール版はそのまま貼り付けてもコンパイルできません。

もっと危ないのは、コンパイラが指摘しない部分です。

TCHAR buffer[256];
GetWindowText(hWnd, buffer, sizeof(buffer));   // 2021年のコード

sizeof はバイト数です。ANSI ビルドなら 256 でたまたま合いますが、Unicode ビルドでは TCHAR が2バイトなので 512 になり、256文字の配列に「512文字まで書いてよい」と伝えてしまいます。タイトルが255文字を超えるとスタック上のバッファを越えて書き込まれます。渡すべきは文字数で、ARRAYSIZE(buffer)(MSVC なら _countof)が正解です。下のように先に長さを聞くほうがさらに確実です。

リテラルのエンコーディングも落とし穴です。GCC はソース中の日本語を UTF-8 のバイト列として埋め込みます。これを ANSI 版の SetWindowTextA に渡すと、日本語版 Windows ではコードページ 932(Shift_JIS 系)として解釈され、文字化けします。W 版と L"..." リテラルを使えばこの問題は起きません。

ほかに直した点は次のとおりです。

  • FindWindow が NULL を返す場合(メモ帳が起動していない場合)を確認していませんでした。
  • WM_PAINT の中で SetWindowText を呼んでいました。描画メッセージはウィンドウが隠れて再表示されるたびに届くので、他アプリへ毎回 WM_SETTEXT を送ることになります。状態を変える処理は、ボタンのクリックなど一度だけ走るイベントに置くべきです。
  • 2021年版は Windows 8 のメモ帳のスクリーンショットを載せていましたが、Windows 11 のメモ帳はタブ付きのアプリに変わっており、今回は確認できていません。そこでサンプルは自分で作ったウィンドウを相手にするよう変えました。

2026年版:先に長さを聞く

// x86_64-w64-mingw32-g++ -municode -Wall -Wextra text2026.cpp -o text2026.exe -luser32
#include <windows.h>
#include <cstdio>
#include <string>

std::wstring GetText(HWND hwnd)                       // 自分のウィンドウ・コントロール
{
    int len = GetWindowTextLengthW(hwnd);             // 実際より大きいことはあっても小さくはない
    if (len <= 0) return {};
    std::wstring s(len + 1, L'\0');                   // +1 = 終端の NUL
    int copied = GetWindowTextW(hwnd, s.data(), len + 1);
    s.resize(copied);                                 // 実際にコピーされた分だけ残す
    return s;
}

std::wstring GetRemoteText(HWND hwnd)                 // 他プロセスのコントロール
{
    DWORD_PTR len = 0;
    if (!SendMessageTimeoutW(hwnd, WM_GETTEXTLENGTH, 0, 0,
                             SMTO_ABORTIFHUNG, 1000, &len) || len == 0)
        return {};
    std::wstring s(len + 1, L'\0');
    DWORD_PTR copied = 0;
    if (!SendMessageTimeoutW(hwnd, WM_GETTEXT, len + 1, (LPARAM)s.data(),
                             SMTO_ABORTIFHUNG, 1000, &copied))
        return {};
    s.resize(copied);
    return s;
}

int wmain()
{
    HWND top = CreateWindowExW(0, L"STATIC", L"Before", WS_OVERLAPPEDWINDOW,
                               CW_USEDEFAULT, CW_USEDEFAULT, 300, 120,
                               nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
    HWND edit = CreateWindowExW(0, L"EDIT", L"", WS_CHILD | WS_VISIBLE,
                                0, 0, 280, 24, top, nullptr, nullptr, nullptr);
    if (!top || !edit) { std::printf("CreateWindowEx failed: %lu\n", GetLastError()); return 1; }

    std::wprintf(L"before: %ls\n", GetText(top).c_str());
    if (!SetWindowTextW(top, L"After — 日本語 한국어"))
        std::printf("SetWindowText failed: %lu\n", GetLastError());
    std::wprintf(L"after:  %zu UTF-16 units\n", GetText(top).size());

    SetWindowTextW(edit, std::wstring(1000, L'x').c_str());   // 256文字より長く
    std::wprintf(L"edit:   %zu chars\n", GetText(edit).size());

    HWND other = FindWindowW(L"Notepad", nullptr);            // なければ nullptr
    if (other) std::wprintf(L"notepad: %ls\n", GetRemoteText(other).c_str());
    DestroyWindow(top);
    return 0;
}

-Wall -Wextra で警告なしにコンパイルでき、生成物は PE32+ executable (console) x86-64, for MS Windows でした。Mac では実行できないため出力は載せていません。 エントリポイントが wmain なので、MinGW では -municode が必要です。

Microsoft Learn によると、GetWindowTextLengthW は ANSI と Unicode が混在する条件で実際より大きい値を返すことがあります。小さい値を返すことはないのでバッファの確保には安全ですが、最終的な長さは GetWindowTextW の戻り値で切り詰めます。上の s.resize(copied) がその処理です。なお std::wstring の長さは UTF-16 の単位数なので、サロゲートペアで表される絵文字などは 2 と数えられます。

配布用にビルドしたときのサイズも測りました(MinGW-w64 14.0.0、GCC 16.2.0、-O2)。

ビルド サイズ 一緒に配布が必要な DLL
既定(動的リンク) 333,405 バイト(strip 後 59,392) libstdc++-6.dll、libgcc_s_seh-1.dll
-static 3,015,335 バイト(strip 後 257,536) なし

DLL の一覧は objdump -p で確認したものです。既定のビルドを exe 単体で別の PC にコピーすると DLL が見つからず起動しないので、1ファイルで渡したいツールなら -static が手軽です。

他プロセスのウィンドウ:GetWindowText がすること・しないこと

判断図。開始:HWND があり、その文字列を読みたい。問い1:そのウィンドウは自プロセスのものか。はいなら GetWindowTextW が WM_GETTEXT を送り、現在の文字列を返す。いいえなら問い2:キャプションのあるトップレベルウィンドウか。はいなら GetWindowTextW はメッセージを送らずにシステムが保持するキャプションを返すので、応答しないプログラムでも待たされない。いいえなら他プログラムのエディットボックスなどのコントロールで、GetWindowTextW は空文字列を返す。この場合は SendMessageTimeoutW で WM_GETTEXT を自分で送る。
どの呼び出しで文字列を読むかは、ウィンドウの持ち主で決まる。GetWindowText が WM_GETTEXT を送るのは自プロセス内だけ。他プロセスなら保存済みのキャプションを返し、他プロセスのコントロールなら空を返す。

Microsoft Learn の GetWindowText のページによると、動作はウィンドウの持ち主で変わります。

  1. 自プロセスのウィンドウ:WM_GETTEXT を送り、ウィンドウプロシージャが返した文字列を受け取ります。
  2. 他プロセスでキャプションあり:メッセージは送らず、システムが保持しているキャプションを返します。相手が応答なしでも呼び出しが止まりません。
  3. 他プロセスのコントロール(キャプションなし):空文字列を返します。ドキュメントにも「他のアプリケーションのコントロールの文字列は取得できない」と明記されています。

3 が必要なら WM_GETTEXT を自分で送ります。ただの SendMessage だと相手が固まっている間こちらも止まるので、SendMessageTimeoutW に SMTO_ABORTIFHUNG とタイムアウトを付けます。WM_GETTEXT はシステムがプロセス間でバッファをコピーしてくれるメッセージなので、ポインタを渡して問題ありません。

2021年版の「他のアプリケーションのコントロールのキャプションは変更できない」という説明は SetWindowText のドキュメントどおりで、今も正しい内容です。ただしトップレベルウィンドウのタイトルは変えられるので、2021年のサンプルはメモ帳のタイトルを書き換えられました。相手が管理者権限で動いている場合は、UIPI(User Interface Privilege Isolation)によってメッセージ自体がブロックされることもあります。

C# から呼ぶ:LibraryImport

.NET 7 以降は、マーシャリングのコードをコンパイル時に生成する LibraryImport が DllImport より推奨されています。Mac 上で net10.0-windows を対象に dotnet build -p:EnableWindowsTargeting=true でコンパイルしました(.NET SDK 10.0.403、ランタイム 10.0.14)。

using System.Runtime.InteropServices;

[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]

nint hwnd = Native.FindWindow("Notepad", null);
Console.WriteLine(hwnd == 0 ? "Notepad not found" : Native.GetText(hwnd));

static partial class Native
{
    [LibraryImport("user32.dll", EntryPoint = "FindWindowW", StringMarshalling = StringMarshalling.Utf16)]
    internal static partial nint FindWindow(string? className, string? windowName);

    [LibraryImport("user32.dll", EntryPoint = "SetWindowTextW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool SetWindowText(nint hWnd, string text);

    [LibraryImport("user32.dll", EntryPoint = "GetWindowTextLengthW", SetLastError = true)]
    internal static partial int GetWindowTextLength(nint hWnd);

    [LibraryImport("user32.dll", EntryPoint = "GetWindowTextW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
    internal static partial int GetWindowText(nint hWnd, Span<char> buffer, int maxCount);

    internal static string GetText(nint hWnd)
    {
        int len = GetWindowTextLength(hWnd);
        if (len <= 0) return "";
        Span<char> buf = len < 256 ? stackalloc char[len + 1] : new char[len + 1];
        int copied = GetWindowText(hWnd, buf, buf.Length);   // +1 = NUL
        return new string(buf[..copied]);
    }
}

最初からこの形で通ったわけではありません。実際に出たエラーは次のとおりです。

error SYSLIB1062: LibraryImportAttribute requires unsafe code. Project must be updated with '<AllowUnsafeBlocks>true</AllowUnsafeBlocks>'.
Program.cs(19,69): error SYSLIB1051: Runtime marshalling must be disabled in this project by applying the 'System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute' to the assembly to enable marshalling this type. The generated source will not handle marshalling of parameter 'buffer'.
  • SYSLIB1062:csproj に <AllowUnsafeBlocks>true</AllowUnsafeBlocks> を追加して解消しました。
  • SYSLIB1051:Span<char>(char[] でも同じ)の引数に StringMarshalling がないと、char を何バイトで渡すか決まらないために出ます。StringMarshalling = StringMarshalling.Utf16 を付けると消えました。

さらに -p:AnalysisMode=All ですべてのアナライザーを有効にすると、関数ごとに CA5392(The method ... didn't use DefaultDllImportSearchPaths attribute for P/Invokes)が計4件出ました。アプリのフォルダーから偽の DLL を読み込む DLL ハイジャックへの対策を求める警告で、上のようにアセンブリへ DefaultDllImportSearchPaths(DllImportSearchPath.System32) を付けると警告0件になりました。比較のため、昔ながらの [DllImport(..., CharSet = CharSet.Auto)] と StringBuilder の宣言もビルドしたところ、CA2101、CA5392、CA1838(Avoid 'StringBuilder' parameters for P/Invokes)の3件が出ました。

ちなみにこのプログラムを Mac でそのまま実行すると、最初の呼び出しで System.DllNotFoundException: Unable to load shared library 'user32.dll' になります。net10.0-windows のプロジェクトはどの OS でもビルドできますが、動作確認は Windows で行う必要があります。

まとめ

  • バッファは GetWindowTextLengthW + 1、単位は文字数。sizeof はバイト数なので Unicode ビルドでは2倍になります。
  • 戻り値 0 は「失敗」と「空のタイトル」の両方。GetLastError で区別します。
  • 他アプリのコントロールは GetWindowText では読めません。SendMessageTimeoutW と WM_GETTEXT を使います。
  • 新しいコードは W 版と L"..." リテラルで。C# なら LibraryImport と StringMarshalling.Utf16 で書きます。

キーの状態を読む API は GetKeyState と GetAsyncKeyState、実行ファイルの場所を得る API は GetCurrentDirectory と GetModuleFileName の記事で同じように検証し直しています。

よくある質問

GetWindowText に渡すバッファの大きさはどう決めますか?

先に GetWindowTextLengthW で長さを取り、終端の NUL 分として 1 を足した文字数のバッファを用意します。第3引数 nMaxCount はバイト数ではなく文字数です。sizeof(buffer) を渡すと Unicode ビルドでは実際の2倍の容量を伝えてしまい、バッファあふれの原因になります。

他のアプリのエディットボックスの文字列が GetWindowText で取れないのはなぜですか?

対象ウィンドウが別プロセスのものだと、GetWindowText は WM_GETTEXT を送らず、システムが保持しているキャプションだけを返します。キャプションのないコントロールは空文字列になります。SendMessageTimeoutW で WM_GETTEXTLENGTH と WM_GETTEXT を自分で送って読みます。

C# では DllImport と LibraryImport のどちらを使いますか?

.NET 7 以降はソースジェネレーター方式の LibraryImport が推奨です。.NET 10 でビルドしたところ、AllowUnsafeBlocks がないと error SYSLIB1062、char バッファに StringMarshalling を指定しないと error SYSLIB1051 になり、両方直すと警告0件でビルドできました。

  • #Win32
  • #SetWindowText
  • #GetWindowText
  • #WM_GETTEXT
  • #LibraryImport

t.laboratory

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

運営者情報 · アプリ

↑