文字の表示

前ページまでで、ウィンドウの作成と終了までのコードを作成しました。
ここから色々な機能を追加して、独自のウィンドウアプリを作成していきます。

BeginPaint関数

まずはウィンドウ上に文字を表示してみます。
文字に限らず、ウィンドウ上への描画を行うにはまずBeginPaint関数を実行します。

HDC BeginPaint(
 HWND hWnd,
 LPPAINTSTRUCT lpPaint
);
ウィンドウhWndの描画の準備をし、lpPaintに必要な情報を格納する。
戻り値はデバイスコンテキストハンドル。

第一引数hWndは描画を開始するウィンドウのハンドルを指定します。
CreateWindow関数で作成した物を指定すれば良いのですが、ウィンドウプロシージャの第一引数HWNDにも同じものが送られてくるのでこれを指定します。

第二引数lpPaintは描画に関する情報を格納する構造体です。
これは後述します。

この関数は画面への描画の準備をします。
この関数を実行してもそれだけでは描画は行われません。

デバイスコンテキストハンドル

BeginPaint関数の戻り値はデバイスコンテキストハンドルというものです。
これはHDC型の変数に格納できます。

デバイスコンテキストとは、ディスプレイやプリンターなどの、画像を表示するためのデバイス情報です。
この情報を格納する変数(HDC構造体)がデバイスコンテキストハンドルです。
これを利用することでプログラマーは各デバイス(パソコンに接続される装置のこと)の違いを気にすることなく、同じ手順で文字や画像を描画することができます。

EndPaint関数

BeginPaint関数は、必要な描画が終わったらEndPaint関数を呼び出す必要があります。

BOOL EndPaint(
 HWND hWnd,
 const LPPAINTSTRUCT lpPaint
);
ウィンドウhWndの描画を終了する。
戻り値は常に0以外。

引数はBeginPaint関数と同じです。

描画のための関数は、BeginPaint関数とEndPaint関数の間に記述する必要があります。


HDC hdc;
PAINTSTRUCT ps;

//描画の開始
//第二引数はアドレスを渡すことに注意
hdc = BeginPaint(hWnd, &ps);

//何らかの描画処理

//描画の終了
//これ以降は描画できない
EndPaint(hWnd, &ps);

TextOut関数

ウィンドウ上に文字を表示するにはTextOut関数を使用します。

BOOL TextOutW(
 HDC hdc,
 int x,
 int y,
 LPCWSTR lpString,
 int c
);
ウィンドウ上の指定の位置に文字を描画する。
成功した場合は0以外を、失敗した場合は0を返す。

第一引数hdcBeginPaint関数で取得したデバイスコンテキストハンドルを指定します。

第二引数、第三は文字を表示する位置です。
クライアント領域の左上を基準としています。
xは横座標、yは縦座標です。

第三引数lpStringは表示する文字列です。

第四引数cは表示する文字数です。
(NULL文字は含まない)

クライアント領域

ウィンドウにはクライアント領域と呼ばれる領域があります。
クライアント領域はプログラマーが自由に使える領域で、ここに文字列を表示したりボタンなどのコントロールを配置します。
クライアント領域の外側のタイトルバーやウィンドウ枠は非クライアント領域(ノンクライアント領域)といい、各アプリケーションで共通の仕様となっています。
プログラマーはいくつかのオプションから非クライアント領域の見た目や機能を選択して使用します。
クライアント領域とウィンドウ領域

赤い斜線の領域がクライアント領域です。
タイトルバーや枠などをすべて含んだ領域をウィンドウ領域と呼ぶこともあります。

TextOut関数の他にもデバイスコンテキストハンドルを通して描画を行う関数が多数用意されています。
これらはGDI関数群といいます。

WM_PAINTメッセージ

BeginPaint関数やTextOut関数の実行は、WM_PAINTメッセージが送信された時に行う必要があります。
つまり、ウィンドウプロシージャ内のWM_PAINTメッセージの処理内で行います。

WM_PAINTメッセージはシステムがウィンドウの描画が必要と判断したときに送られるメッセージです。
例えばウィンドウがディスプレイの端からはみ出した場合やウィンドウサイズを変更した場合など、ウィンドウの一部が欠けた時にウィンドウ内の再描画が必要となります。
再描画が必要なときにWM_PAINTメッセージが発行されますので、そのタイミングで内容を描く処理をすれば良いわけです。

ちなみにウィンドウ作成直後は「何も描画されていない状態」なので描画が必要な状態と判断され、WM_PAINTメッセージが送られてきます。

サンプルコード1

ここまでのサンプルコードです。


#include <windows.h>

//グローバル変数
HINSTANCE hInst;	//インスタンス
WCHAR szTitle[] = L"テストアプリ";		//タイトル
WCHAR szWindowClass[] = L"MyTestApp";	//ウィンドウクラス名

//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(
	HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPWSTR lpCmdLine,
	int nCmdShow)
{
	//省略
}

//ウィンドウクラスの登録
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	//省略
}

//ウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
	//省略
}

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;

	switch (message)
	{
	case WM_PAINT: //ウィンドウの描画発生
		hdc = BeginPaint(hWnd, &ps);
		TextOut(hdc, 10, 10, L"文字の出力", 5);
		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

ビルドして実行すると、クライアント領域の左上から(10, 10)の位置に「文字の出力」という文字列が表示されます。
TextOut関数のサンプル

PAINTSTRUCT構造体

BeginPaint/EndPaint関数の両方で使用されるPAINTSTRUCTは構造体で、描画のための情報を保持します。
(先頭の「LP」はポインタの意味です)

typedef struct tagPAINTSTRUCT {
 HDC hdc;
 BOOL fErase;
 RECT rcPaint;
 BOOL fRestore;
 BOOL fIncUpdate;
 BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
アプリケーションがクライアント領域を描画するのに必要な情報を保持する構造体。

hdcメンバは、BeginPaint関数の戻り値と同じものが格納されます。

fEraseメンバは背景の消去を行うかのフラグです。

rcPaintメンバは描画を行う領域です。

fRestorefIncUpdatergbReservedメンバはシステムが使用するものでプログラマが何か設定したり参照したりすることはありません。

この構造体はBeginPaint関数によってシステムから各メンバの値がセット済みの変数が渡されます。
プログラマは必要に応じて値を参照して使用します。

画面の更新

ウィンドウ画面上の文字列を書き換える場合、TextOut関数に文字列変数を指定し、その変数の中身を書き換えることになります。
しかし、単に変数の中身を変更しただけでは文字列は書き換わりません。

これは、画面の描画はWM_PAINTが送られてこないと行われないためです。
WM_PAINTメッセージは「システムが」ウィンドウの再描画が必要と判断したときに送られてきます。
変数の中身を書き換えてもWM_PAINTメッセージが送られてくるわけではないので、画面は再描画されず文字列も書き換わらないのです。

InvalidateRect関数

システムにウィンドウの再描画をリクエストするにはInvalidateRect関数を使用します。

BOOL InvalidateRect(
 HWND hWnd,
 const RECT *lpRect,
 BOOL bErase
);
ウィンドウhWndに、矩形lpRectの無効領域を設定する。
lpRectがNULLの場合はクライアント領域全てを無効領域にする。
bEraseがTRUEの場合は背景を塗りつぶす。
成功した場合は0以外を返す。
失敗した場合は0を返す。

この関数は、クライアント領域に無効領域を作ります。
無効領域が発生すると、システムはWM_PAINTメッセージを送信します。

第一引数hWndは無効領域を作るウィンドウのハンドルです。

第二引数lpRectRECT構造体のポインタです。
ここにNULLを指定するとクライアント領域全体を無効領域に設定できます。
(RECT構造体については後述)

第三引数bEraseは背景の消去をするか否かです。
ここにTRUEをセットすると、BeginPaint関数の実行時に背景が消去され、再描画が行われます。
BeginPaint関数実行時に渡されるPAINTSTRUCT構造体のfEraseメンバがTRUEにセットされます。

無効領域

ウィンドウの描画はクライアント領域全てに対して行うとは限りません。
例えば領域の一部が欠けた場合、その欠けた箇所だけを再描画すればよく、そのほうが効率が良くなります。
再描画が必要な領域を無効領域(更新領域)といい、WM_PAINTメッセージは無効領域が発生したときに送られてきます。

無効領域はPAINTSTRUCT構造体のrcPaintメンバにセットされます。
つまり、InvalidateRect関数の第二引数lpRectに無効領域を指定すると、その値がBeginPaint関数実行時にPAINTSTRUCT構造体のrcPaintメンバに格納されるわけです。

BeginPaint関数は、無効領域のみを描画対象とし、それ以外の領域は描画しません。

RECT構造体

typedef struct tagRECT {
 LONG left;
 LONG top;
 LONG right;
 LONG bottom;
} RECT, *PRECT, *NPRECT, *LPRECT;
矩形情報を保持する構造体。

RECT構造体は矩形(長方形)の情報を保存します。
各メンバはそれぞれ基準点からの位置を示します。
leftメンバとtopメンバで矩形の左上頂点を、rightメンバとbottomメンバで右下頂点を表します。
基準点はディスプレイであったりウィンドウ内であったりと様々です。
RECT構造体

サンプルコード2

画面を更新するサンプルコードです。


#include <windows.h>
#include <strsafe.h>

#define BUFFERSIZE 32

//ウィンドウの生成等は省略

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;

	static WCHAR buf[BUFFERSIZE];
	static int count;

	switch (message)
	{
	case WM_PAINT: //ウィンドウの描画発生
		hdc = BeginPaint(hWnd, &ps);
		TextOut(hdc, 10, 10, buf, lstrlen(buf));
		EndPaint(hWnd, &ps);
		break;

	case WM_LBUTTONUP: //マウス左クリック
		//文字列の整形
		StringCchPrintf(buf, BUFFERSIZE, L"左クリックの回数: %d", ++count);

		//無効領域を作る
		InvalidateRect(hWnd, NULL, TRUE);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

このコードはウィンドウ上をクリックする毎に数字がカウントアップしていきます。
ウィンドウクリック時に文字列変数を書き換え、次に無効領域を発生させているので、WM_PAINTメッセージが発行され、表示されている文字列が書き換わるわけです。
マウス左クリックの回数をカウントするプログラム

注意点として、InvalidateRect関数はWM_PAINTメッセージの処理内で実行してはいけません。
WM_PAINTメッセージは無効領域があると送られてくるので、その処理内で再度無効領域を発生させると再度WM_PAINTが送信され、無限ループになってしまいます。

Windows APIの文字列処理関数

上記コードではWindows APIが提供する文字列操作関数であるStringCchPrintf関数とlstrlen関数を使用しています。
C/C++言語用の文字列操作関数を使用することもできますが、特にワイド文字を使用する場合はWindows APIを使用したほうが簡単です。
文字列操作関数は文字列操作関数の項で改めて説明しますが、簡単に説明しておきます。

C/C++用には高度なライブラリが提供されていたりするので、Windows APIでカバーされていない処理はそれらを利用しても良いでしょう。

StringCchPrintf関数

StringCchPrintf関数はsprintf_s関数と同じで、書式指定文字列を使用して文字列を整形します。
この関数の使用にはstrsafe.hのインクルードが必要です。

STRSAFEAPI StringCchPrintfW(
 STRSAFE_LPWSTR pszDest,
 size_t cchDest,
 STRSAFE_LPCWSTR pszFormat,
 ...
);
書式指定文字列pszFormatを使用して変換した文字列を文字列バッファpszDestにcchDest文字分書き込む。

第一引数pszDestは書き込み先のバッファです。
第二引数cchDestは書き込む文字数です。
(バイト数ではない)
第三引数pszFormatは書式指定文字列です。
書式の指定(変換指定子)はprintf関数と同じです。
第四引数以降には書式指定文字列で使用する値を必要なだけ指定します。

lstrlen関数

lstrlen関数はstrlen関数と同じで、文字列の文字数を数えます。
(バイト数ではない)

int lstrlenW(
 LPCWSTR lpString
);
文字列lpStringの文字数を返す。
(NULL文字は含まない)