CreateWindow・ShowWindow:メッセージループとDPI対応まで書くWin32
CreateWindowEx・ShowWindowでウィンドウを出す完全なコードとメッセージループ、WM_DESTROY、DPI対応、ANSI/Unicodeのコンパイルエラー、C# LibraryImport版まで解説。
2026年11月改訂: この記事は2021年に韓国語で書いた CreateWindow・ShowWindow の講座を、日本語向けに書き直したものです。元の例は Visual Studio テンプレートの
InitInstance関数の一部だけで、1つ目は閉じ波かっこが抜けており、2つ目は MinGW-w64 の既定設定でcannot convert 'WCHAR*' to 'LPCSTR'になりコンパイルできませんでした。今回はクラス登録からメッセージループ、終了処理、DPI 対応までを含む完全なプログラムと、C# のLibraryImport版を新しく書いています。すべて MinGW-w64(GCC 16.2.0)または .NET 10 SDK で Windows 向けにコンパイルしましたが、執筆に使った Mac では実行していません。実行時の動きは Microsoft Learn に書かれた仕様をもとに説明します。
Win32 API でウィンドウを1枚出したいのに、「閉じてもプロセスが終わらない」「4K ディスプレイで文字がにじむ」「サンプルを貼ったらコンパイルが通らない」と詰まった人向けの記事です。先に結論を書くと、CreateWindowEx はウィンドウを作るだけ、ShowWindow が表示し、その後の仕事はすべてメッセージループとウィンドウプロシージャがします。どれか1つでも欠けると上の症状が出ます。
CreateWindow は関数ではなくマクロ
2021年版は CreateWindow を関数として紹介していましたが、winuser.h ではマクロです。CreateWindow(...) は拡張スタイル0の CreateWindowEx(0, ...) になり、さらに UNICODE の有無で CreateWindowExW か CreateWindowExA に分かれます。また元の記事では10番目の引数を HANDLE と書いていましたが、正しくは HINSTANCE です。
HWND CreateWindowExW(
DWORD dwExStyle, // WS_EX_...。CreateWindow は0を渡す
LPCWSTR lpClassName, // RegisterClassExW で登録したクラス名か ATOM
LPCWSTR lpWindowName, // タイトルバーの文字列
DWORD dwStyle, // WS_OVERLAPPEDWINDOW など
int X, int Y, int nWidth, int nHeight,
HWND hWndParent, // 親(またはオーナー)ウィンドウ
HMENU hMenu, // メニュー。子ウィンドウならコントロールID
HINSTANCE hInstance,
LPVOID lpParam); // WM_CREATE の CREATESTRUCT.lpCreateParams に届く
スタイルの表は元の記事でおおむね合っていました。実際によく使うものだけ挙げます。
| スタイル | 意味 |
|---|---|
WS_OVERLAPPEDWINDOW |
普通のウィンドウ。タイトルバー、システムメニュー、サイズ変更枠、最小化・最大化ボタン |
WS_POPUP |
タイトルバーのないポップアップ。WS_CHILD とは併用不可 |
WS_CHILD |
ボタンやエディットなどの子ウィンドウ。hWndParent が必須 |
WS_VISIBLE |
作成と同時に表示。ShowWindow を別に呼ばなくてよい |
WS_CLIPCHILDREN |
親の描画で子の領域を除外(ちらつき軽減) |
WS_BORDER |
細い枠。サイズ変更不可 |
元の2つ目の例は WS_BORDER だけを指定していました。WS_OVERLAPPED の値は0なので、これもオーバーラップウィンドウとして作られ、Windows はオーバーラップウィンドウに必ずタイトルバーを付けます。当時のスクリーンショットにタイトルバーがあったのはそのためで、なくなるのはサイズ変更と最小化・最大化ボタンです。
2021年のコードを MinGW でコンパイルすると
元の例は Visual Studio の「Windows デスクトップ アプリケーション」テンプレートから InitInstance を切り出したものなので、単体ではコンパイルできません。テンプレートと同じグローバル変数(WCHAR szWindowClass[100] など)を足して x86_64-w64-mingw32-g++ に通しました。
1つ目は関数末尾の } がありませんでした。
create2021_first.cpp:23:16: error: expected '}' at end of input
23 | return TRUE;
| ^
かっこを補った2つ目は、MinGW の既定設定で次のエラーになりました。
create2021.cpp:14:24: error: cannot convert 'WCHAR*' {aka 'wchar_t*'} to 'LPCSTR' {aka 'const char*'}
14 | hWnd = CreateWindow(szWindowClass, L"타이틀", WS_BORDER,
| ^~~~~~~~~~~~~
…/winuser.h:2203:65: note: initializing argument 2 of 'HWND__* CreateWindowExA(DWORD, LPCSTR, LPCSTR, …)'
原因は ANSI と Unicode の混在です。Visual Studio のプロジェクトは構成プロパティの「文字セット」が「Unicode 文字セットを使用する」になっていて UNICODE が定義されるため、マクロは CreateWindowExW を選び、WCHAR 配列も L"..." もそのまま通ります。MinGW は何も定義しないので CreateWindowExA が選ばれ、char の文字列を要求します。-DUNICODE -D_UNICODE を付けると同じファイルが警告なしで通りました。
ビルドで UNICODE を定義してもよいのですが、この記事のおすすめは CreateWindowExW、RegisterClassExW、DefWindowProcW のように W 版を直接呼ぶことです。ビルド設定が変わっても意味が変わらず、日本語のタイトルが Shift_JIS(CP932)を経由して化けることもありません。
メッセージループと終了処理まで入れた完全なコード
元の記事は最後に「クラス定義 → 登録 → 作成 → 表示 → メッセージループ」と順番だけを書いていました。全部を1ファイルにすると次のとおりです。
// window.cpp — Unicode、Per-Monitor V2 DPI 対応(Windows 10 1703 以降)
#include <windows.h>
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
TextOutW(hdc, 16, 16, L"Hello, Win32", 12);
EndPaint(hwnd, &ps);
return 0;
}
case WM_DPICHANGED: { // 拡大率の違うモニターへ移動した
RECT* r = reinterpret_cast<RECT*>(lp);
SetWindowPos(hwnd, nullptr, r->left, r->top, r->right - r->left,
r->bottom - r->top, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0); // これがないとプロセスが終わらない
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int nCmdShow)
{
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = WndProc;
wc.hInstance = hInst;
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
wc.lpszClassName = L"TLabDemoWindow";
if (!RegisterClassExW(&wc)) return 1;
UINT dpi = GetDpiForSystem(); // 96 = 100%、144 = 150%
HWND hwnd = CreateWindowExW(0, L"TLabDemoWindow", L"Demo",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
MulDiv(500, dpi, 96), MulDiv(300, dpi, 96),
nullptr, nullptr, hInst, nullptr);
if (!hwnd) return 1;
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0) > 0) { // 0 = WM_QUIT、-1 = エラー
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return static_cast<int>(msg.wParam);
}
$ x86_64-w64-mingw32-g++ -std=c++17 -Wall -Wextra -O2 -municode -mwindows window.cpp -o window.exe -luser32 -lgdi32
$ file window.exe
window.exe: PE32+ executable (GUI) x86-64, for MS Windows
-Wall -Wextra でも警告は0件でした。押さえておきたい点があります。
-municodeを外すとリンクエラー(undefined reference to 'WinMain')になります。MinGW はこのオプションがあるときだけwWinMainをエントリポイントにします。-DUNICODEを足しても結果は同じでした。さらにUNICODEなしではIDC_ARROWがchar*の定数になり、LoadCursorWに渡すところでcannot convert 'LPSTR' to 'LPCWSTR'になりました。Unicode の設定が中途半端だと、思わぬ場所で壊れます。while (GetMessage(...))とは書かないこと。GetMessageはエラー時に -1 を返し、-1 は真なので無限ループになります。Microsoft Learn の GetMessage のページにも同じ注意があります。> 0と比べれば WM_QUIT(0)でもエラー(-1)でも抜けます。- 最初の
ShowWindowにはnCmdShowをそのまま渡します。 ショートカットの「実行時の大きさ:最小化」などはこの値で届きます。元の表にあったSW_FORCEMINIMIZEの「Windows 2000 以降のみ」という注記は、今は気にしなくてかまいません。 UpdateWindowはWM_PAINTをすぐ送って最初の描画を済ませます。 なくてもいずれ描画されますが、テンプレートの慣例です。
WM_DESTROY と PostQuitMessage の役割
閉じるボタンを押すと WM_CLOSE が届き、DefWindowProcW がそれを受けて DestroyWindow を呼びます。破棄の途中で WM_DESTROY が来るので、そこで PostQuitMessage(0) を呼ぶと、スレッドのキューに WM_QUIT が入り、GetMessageW が0を返してループが終わります。
この1行を忘れると、ウィンドウは消えたのにプロセスだけが残ります。ループが来ない WM_QUIT を待って眠り続けるからです。逆に、ウィンドウを複数持つアプリでサブウィンドウまで PostQuitMessage を呼ぶと、サブを1枚閉じただけでアプリ全体が終わります。呼ぶのはメインウィンドウだけが原則です。「保存しますか?」で閉じるのを取り消したいときは、WM_CLOSE を処理して DestroyWindow を呼ばなければよいだけです。
DPI 対応はマニフェストが第一候補
DPI 非対応のプログラムは 96 DPI で描いた画面を Windows がビットマップとして拡大するため、150% のディスプレイでは文字がにじみます。2021年版にはなかった話ですが、今はノート PC のほとんどが100%以外の拡大率で出荷されるので外せません。
上のコードはウィンドウを作る前に SetProcessDpiAwarenessContext(Windows 10 1703 以降)を呼んでいます。ただし Microsoft Learn は API よりもアプリケーションマニフェストでの指定を推奨しています。プロセス起動時点で決まるので、呼ぶ順番を間違える余地がありません。MinGW ではリソースとして埋め込みます。
<!-- app.manifest -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
$ printf '#include <winuser.h>\nCREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "app.manifest"\n' > app.rc
$ x86_64-w64-mingw32-windres app.rc -O coff -o app.res.o
$ x86_64-w64-mingw32-g++ … window.cpp app.res.o -o window_m.exe -luser32 -lgdi32
できた exe に .rsrc セクションがあり、その中に PerMonitorV2 の文字列が入っていることまで確認しました。dpiAwareness を知らない Windows 10 1607 より前の版は dpiAware の行を読みます。マニフェストで指定済みなら API の呼び出しは失敗する(ドキュメント上は ERROR_ACCESS_DENIED)ので、どちらか片方にします。
DPI 対応を宣言すると、拡大の面倒は自分で見ることになります。500×300 の固定値では150%のモニターで小さく見えるため MulDiv(500, dpi, 96) で換算し、別の拡大率のモニターへ移ったときに届く WM_DPICHANGED の lParam(推奨サイズの RECT)をそのまま適用しています。フォントやアイコンも DPI に合わせて作り直す必要があり、ここが素の Win32 でいちばん手間のかかる部分です。
C# の LibraryImport で同じウィンドウを作る
.NET からも P/Invoke で同じことができます。.NET 7 以降は、実行時にマーシャリングのスタブを作る DllImport より、ソースジェネレーターがコンパイル時にコードを生成する LibraryImport が推奨です。たとえば FindWindow を DllImport で宣言し、SYSLIB1054 を警告に上げると、アナライザーがこう教えてくれます。
warning SYSLIB1054: Mark the method 'FindWindow' with 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
SYSLIB1054 は既定の重大度が「情報」で、普通のビルド出力には出ません。.editorconfig に dotnet_diagnostic.SYSLIB1054.severity = warning を書いて表示させました。以下は net10.0-windows、<AllowUnsafeBlocks>true</AllowUnsafeBlocks>、<OutputType>WinExe</OutputType> のプロジェクトの要点です(構造体や宣言の一部は … で省略)。
using System.Runtime.InteropServices;
unsafe
{
Native.SetProcessDpiAwarenessContext(-4); // PER_MONITOR_AWARE_V2
nint hInst = Native.GetModuleHandleW(null);
fixed (char* cls = "TLabDemoWindowCs")
{
var wc = new WNDCLASSEXW
{
cbSize = (uint)sizeof(WNDCLASSEXW),
lpfnWndProc = &WndProc, // [UnmanagedCallersOnly] の static 関数
hInstance = hInst,
hCursor = Native.LoadCursorW(0, 32512), // IDC_ARROW
hbrBackground = 5 + 1, // COLOR_WINDOW + 1
lpszClassName = cls,
};
if (Native.RegisterClassExW(in wc) == 0) return 1;
}
uint dpi = Native.GetDpiForSystem();
nint hwnd = Native.CreateWindowExW(0, "TLabDemoWindowCs", "Demo (C#)",
0x00CF0000 /* WS_OVERLAPPEDWINDOW */, unchecked((int)0x80000000), unchecked((int)0x80000000),
(int)(500 * dpi / 96), (int)(300 * dpi / 96), 0, 0, hInst, 0);
if (hwnd == 0) return 1;
Native.ShowWindow(hwnd, 1); // SW_SHOWNORMAL
MSG msg;
while (Native.GetMessageW(&msg, 0, 0, 0) > 0)
{
Native.TranslateMessage(&msg);
Native.DispatchMessageW(&msg);
}
return (int)msg.wParam;
}
[UnmanagedCallersOnly]
static nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam)
{
if (msg == 0x0002) { Native.PostQuitMessage(0); return 0; } // WM_DESTROY
return Native.DefWindowProcW(hwnd, msg, wParam, lParam);
}
static unsafe partial class Native
{
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
public static partial nint CreateWindowExW(uint exStyle, string cls, string title, uint style,
int x, int y, int w, int h, nint parent, nint menu, nint inst, nint param);
[LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)]
public static partial bool ShowWindow(nint hwnd, int cmd);
[LibraryImport("user32.dll")]
public static partial int GetMessageW(MSG* msg, nint hwnd, uint min, uint max);
// … RegisterClassExW、TranslateMessage、DispatchMessageW、DefWindowProcW など
}
$ dotnet build -c Release -r win-x64 --self-contained false -p:EnableWindowsTargeting=true
Build succeeded.
$ file bin/Release/net10.0-windows/win-x64/WinCreate.exe
WinCreate.exe: PE32+ executable (GUI) x86-64, for MS Windows
.NET SDK 10.0.403 で警告なしにビルドできました(こちらも実行はしていません)。LibraryImport へ移す途中で実際に出た診断は次のとおりです。
| うっかり | 実際の診断 |
|---|---|
クラスに partial がない |
SYSLIB1050、CS0751、CS8795 |
string 引数に StringMarshalling がない |
error SYSLIB1051: Marshalling string or char without explicit marshalling information is not supported |
bool の戻り値に MarshalAs がない |
error SYSLIB1051: Marshalling bool without explicit marshalling information is not supported |
プロジェクトに AllowUnsafeBlocks がない |
error SYSLIB1062: LibraryImportAttribute requires unsafe code |
ウィンドウプロシージャはデリゲートではなく、[UnmanagedCallersOnly] を付けた static 関数の関数ポインター(delegate* unmanaged<...>)で渡しました。ネイティブ側に渡したデリゲートは、Windows が呼び続ける間 GC に回収されないよう保持しておく必要がありますが、static 関数のポインターならその心配がありません。デリゲートとコールバックそのものは C# のデリゲートとコールバック で扱っています。
実測:実行ファイルのサイズと、2026年に何を選ぶか
MinGW-w64 GCC 16.2.0、-O2 でビルドした上の C++ 版のサイズです。
| ビルド | サイズ |
|---|---|
window.exe(デバッグシンボル付き) |
154,440 バイト |
window_s.exe(-s でシンボル削除) |
17,920 バイト |
window_m.exe(-s + DPI マニフェスト) |
18,944 バイト |
C# の WinCreate.dll + アプリホスト WinCreate.exe |
8,192 + 162,304 バイト(別途 .NET 10 ランタイムが必要) |
シンボルを削った C++ 版は 19KB 弱で、インポートしている DLL は USER32、GDI32、KERNEL32 と、Windows 10 以降に標準で入っている UCRT(api-ms-win-crt-*)だけでした。インストーラーなしでコピーするだけで動く小さなツールなら、今でも Win32 C++ がいちばん身軽です。
とはいえ、新しいデスクトップアプリを素の Win32 で始める理由はあまりありません。ボタン、フォント、DPI の変化まですべて自分で扱い、アクセシビリティ(スクリーンリーダー)やダークモードも自前です。業務ツールなら .NET 10 の WinForms か WPF のほうがずっと速く作れ、Windows 11 らしい見た目が欲しければ Windows App SDK の WinUI 3 が合います。どれも内部では CreateWindowEx とメッセージループの上に乗っているので、この記事の構造は「UI スレッドが固まった」ときの調査にそのまま役立ちます。
他のプログラムのウィンドウハンドルを探す方法は FindWindow と EnumWindows、そのウィンドウにデッドロックせずメッセージを送る方法は SendMessage と PostMessage に続きます。
検証環境:macOS(Apple Silicon)上の MinGW-w64 x86_64-w64-mingw32-g++ (GCC) 16.2.0、.NET SDK 10.0.403(EnableWindowsTargeting=true)。Windows 向けにコンパイルのみ行い、実行はしていません。
よくある質問
ウィンドウを閉じてもプロセスが残るのはなぜ?
WM_DESTROY で PostQuitMessage(0) を呼んでいないからです。ウィンドウが消えても GetMessage のループは WM_QUIT を受け取るまで終わらないため、プロセスがタスクマネージャーに残ります。メインウィンドウのウィンドウプロシージャで WM_DESTROY を受けたら PostQuitMessage(0) を呼びます。
MinGW で CreateWindow に L"…" を渡すとコンパイルエラーになるのはなぜ?
MinGW は既定で UNICODE を定義しないので、CreateWindow マクロが CreateWindowExA(char 文字列)に展開されます。wchar_t の文字列を渡すと cannot convert 'WCHAR*' to 'LPCSTR' になります。Visual Studio のプロジェクトは「Unicode 文字セットを使用する」が既定なので同じコードが通っていました。CreateWindowExW など W 版の関数を直接呼べば設定に左右されません。
高解像度ディスプレイでウィンドウがぼやけます。
DPI 非対応のプログラムは Windows がビットマップとして引き伸ばして表示するのでぼやけます。マニフェストに dpiAwareness PerMonitorV2 を書くか、ウィンドウを作る前に SetProcessDpiAwarenessContext を呼び、サイズを DPI で換算して WM_DPICHANGED に対応します。Microsoft は API よりマニフェストを推奨しています。