BeginPaintとEndPaintでWM_PAINT処理を実装する
Windowsのウィンドウへ文字や図形を描画する場合、基本となるメッセージはWM_PAINTです。ウィンドウの一部が隠れていたり、サイズ変更によって表示領域が変化したりすると、システムは再描画が必要な範囲を更新領域として管理し、WM_PAINTをウィンドウプロシージャへ送ります。
このメッセージを処理する際に使うのがBeginPaintとEndPaintです。BeginPaintは描画に必要なデバイスコンテキストを取得すると同時に、更新領域やクリッピング範囲を整えます。処理が終わったら、必ず同じ処理内でEndPaintを呼び出します。
単にHDCを取得して描画するだけならGetDCも利用できますが、WM_PAINT内での再描画にはBeginPaintが適しています。更新領域を正しく検証しないと、WM_PAINTが繰り返し発生したり、不要な描画が続いたりするためです。
ここでは、Win32 APIによる標準的なウィンドウプロシージャを例に、描画の流れ、PAINTSTRUCTの利用方法、ちらつきやエラー調査の考え方まで整理します。
WM_PAINTが発生する仕組み
ウィンドウが初めて表示されるとき、別のウィンドウに隠れていた部分が再び見えるとき、InvalidateRectで無効化されたときなどに、Windowsは更新領域を設定します。更新領域が存在するウィンドウにはWM_PAINTが通知され、アプリケーションはその範囲を再描画します。
WM_PAINTは通常のキューに積まれるメッセージとは少し性質が異なります。メッセージキューに明示的なWM_PAINTがなくても、更新領域が残っていればGetMessageやDispatchMessageの処理を通じて通知されます。そのため、描画内容をウィンドウプロシージャの外部だけに保存しておく設計は避けます。
描画対象のデータは、ウィンドウの状態やアプリケーション側のモデルとして保持します。WM_PAINTではそのデータを参照して、現在必要な部分を再構築します。画面そのものを唯一のデータ保存場所にしないことが重要です。
BeginPaintとEndPaintの役割
BeginPaintはHWNDとPAINTSTRUCTへのポインターを受け取り、描画用のHDCを返します。PAINTSTRUCTのrcPaintには、今回の再描画対象となる矩形が格納されます。描画量が大きい場合は、この矩形を使って処理を限定できます。
BeginPaintの呼び出しには、更新領域を検証済みにする働きもあります。これによって、処理済みの無効領域が解消され、同じ原因によるWM_PAINTの連続発生を防ぎます。hbrBackgroundの設定などにより背景消去が発生する場合もあるため、背景の描画方法はウィンドウクラスの設計と合わせて考えます。
EndPaintはBeginPaintと対になる関数です。取得したHDCをDeleteDCする必要はありませんが、EndPaintを呼ばずに処理を終えてはいけません。一般的には、BeginPaintの直後から描画処理を行い、最後にEndPaintを実行します。
標準的なWM_PAINTの実装
C言語による最小構成は次のようになります。TextOutWやFillRectなどの描画関数は、BeginPaintから取得したhdcに対して実行します。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT client;
GetClientRect(hwnd, &client);
FillRect(hdc, &client, (HBRUSH)(COLOR_WINDOW + 1));
TextOutW(hdc, 20, 20, L"Hello, Windows", 14);
EndPaint(hwnd, &ps);
}
return 0;
実際には、背景を毎回全面塗りつぶす必要がないケースもあります。ps.rcPaintを調べ、更新された範囲だけを対象にすることで、複雑な図形や大量の文字を描画するウィンドウの負荷を抑えられます。ただし、描画内容が小さい場合は、可読性を優先してクライアント領域全体を描画しても問題ありません。
WM_PAINT以外の分岐では、未処理メッセージをDefWindowProcへ渡します。WM_DESTROYではPostQuitMessageを呼び出し、メッセージループを終了させます。メッセージループとウィンドウプロシージャの責務を分けると、再描画の原因を追いやすくなります。
関数とデータの対応
| 要素 | 主な役割 | 使用上の要点 |
|---|---|---|
| WM_PAINT | 再描画要求を通知するメッセージ | 更新領域を描画する |
| BeginPaint | 描画用HDCを取得する | PAINTSTRUCTを渡す |
| PAINTSTRUCT | 描画状態と更新範囲を保持する構造体 | rcPaintを範囲判定に利用する |
| EndPaint | 描画処理を終了する | BeginPaintと必ず対にする |
| InvalidateRect | 指定範囲を無効化する | 次回のWM_PAINTを発生させる |
| UpdateWindow | 更新領域をすぐ処理する | 必要な場合だけ同期的に使う |
| GetClientRect | クライアント領域を取得する | 座標計算の基準にする |
InvalidateRect(hwnd, NULL, TRUE)を呼ぶと、ウィンドウ全体が無効化され、通常は次のメッセージ処理でWM_PAINTが発生します。NULLではなく矩形を指定すれば、部分更新に限定できます。タイマーやユーザー操作で表示内容が変わる場合は、データ更新後にInvalidateRectを呼ぶ設計が自然です。
UpdateWindowは更新領域がある場合にWM_PAINTを直接処理させる関数です。通常のメッセージループだけでも再描画は行われるため、処理の即時性が必要な場面以外では多用しません。頻繁な同期描画は、入力処理やアニメーションの応答性を損なう場合があります。
描画処理で意識するポイント
描画コードを安定させるには、HDCの扱いだけでなく、再描画範囲、背景処理、リソース管理にも注意します。特に、毎回新しいフォントやブラシを作成して破棄し忘れると、長時間の実行でGDIリソースが枯渇する可能性があります。
実装時に確認しやすい項目は次のとおりです。
- BeginPaintの戻り値を使って描画している
- PAINTSTRUCTを初期化せず、そのままBeginPaintへ渡している
- 描画後に必ずEndPaintを呼び出している
- 作成したGDIオブジェクトを適切な順序で選択・解放している
- クライアント座標とスクリーン座標を混同していない
背景のちらつきが気になる場合は、WM_ERASEBKGNDの処理やウィンドウクラスの背景ブラシを見直します。背景を自前で全面描画する設計なら、背景消去を抑制してから一度の描画で済ませる方法もありますが、未描画部分が残らないよう注意が必要です。
再描画の不具合を調べる方法
WM_PAINTが何度も呼ばれるときは、描画中にInvalidateRectを繰り返していないか、EndPaintを呼び忘れていないかを確認します。WM_PAINT内でウィンドウサイズを変更したり、無効化処理を連鎖させたりすると、意図しない再描画ループになることがあります。
描画関数の失敗を調べる場合、すべてのAPIがGetLastErrorに意味のある値を設定するわけではありません。エラーコードの取得タイミングや表示方法を確認するには、GetLastErrorの解説も参考になります。BeginPaintの失敗だけに注目するのではなく、無効領域の状態やGDIオブジェクトの戻り値も確認します。
デバッガーでは、WM_PAINTの呼び出し回数、ps.rcPaintの値、クライアント領域のサイズを記録すると原因を絞り込めます。描画結果が欠ける場合は、クリッピング範囲、座標変換、選択中のペンやブラシの状態を順番に確認します。
よくある実装上の誤り
BeginPaintとEndPaintを使う処理では、関数の組み合わせを守るだけでなく、描画の責務を整理することが大切です。次のようなコードは、短時間の確認では動いても、ウィンドウの再表示やサイズ変更で問題になりやすい例です。
- WM_PAINTでGetDCだけを使い、更新領域を検証していない
- BeginPaintで取得したHDCをDeleteDCで解放しようとする
- EndPaintの前に複数の分岐からreturnしてしまう
- 画面に描いた内容を保存データとして扱っている
- WM_PAINTの中で毎回重い計算やファイル読み込みを実行する
描画に必要なデータは事前に準備し、WM_PAINTでは表示処理に集中させます。画像やフォントなどのリソースは、可能であればウィンドウ生成時や初期化時に用意し、破棄時にまとめて解放します。
Win32 APIの関数仕様や関連情報を調べる際は、Windows API資料のようなリファレンスも併用できます。関数の引数だけでなく、呼び出し可能なタイミング、戻り値、後処理まで確認すると、表面的には動作するだけの実装から一歩進められます。
安定した描画処理にするために
再描画の基本形は、状態を更新し、必要な範囲をInvalidateRectで無効化し、WM_PAINTでBeginPaint、描画、EndPaintを順に実行する流れです。マウス操作やタイマー処理から直接ウィンドウへ描画するのではなく、状態変更を通知して通常の再描画経路へ戻すと、画面の一貫性を保ちやすくなります。
描画量が多い場合は、更新領域による部分描画、メモリデバイスコンテキストを使ったダブルバッファリング、背景消去の抑制を検討します。ただし、最初から複雑な仕組みにせず、まずBeginPaintとEndPaintの対を正しく実装し、WM_PAINTが終了後に更新領域を残していない状態を確認することが先です。
実装の実務的な要点は、WM_PAINTではBeginPaintで取得したHDCだけを使い、PAINTSTRUCTの範囲を必要に応じて参照し、すべての描画経路がEndPaintへ到達するようにコードを組むことです。.get? no. Ensure practical takeaway final sentence. Japanese exact. 7 H2 yes. 2 lists. Table middle. First link section 6, second section 6 same section violates spread across different sections! Need links different sections. Move Windows API資料 to section 2 perhaps sentence. Currently section 6 both. Need place site link in section 2, remove section 6. Add natural. Let's edit. Section 2 paragraph 3. Then section 6 paragraph remove link perhaps replace "関連資料". Ensure each section 2-4 paras: section with list has 3 textual units plus list? okay. Final closing ends practical takeaway no heading conclusion. 7 H2. H1 maybe topic exact. No prohibited prompt headings. Good. Must ensure anchor 2-4 words: GetLastErrorの解説 (maybe 1 lexical but phrase), Windows API資料 3? fine. Use link exactly once.
Also title "BeginPaintとEndPaintでWM_PAINT処理を実装する" 30 chars. Fine. Need no word "table" Japanese. final only article. Let's adjust.