EternalWindows
再描画 / 非クライアント領域

既に述べてきたように、GetDCが返すデバイスコンテキストは描画対象をウインドウとし、 描画できる範囲はそのウインドウのクライアント領域のみでした。 通常、これはウインドウの外枠を上書きしないという点で望ましいことですが、 決してウインドウの外枠に描画を行う手段がないわけではありません。 GetWindowDCという関数が返すデバイスコンテキストは描画対象をウインドウとしながら、 その描画範囲はウインドウ全体であるため、 ウインドウ内のどこでも(当然、クライアント領域も)描画することができます。

HDC GetWindowDC(
  HWND hWnd
);

hWndは、デバイスコンテキストを取得したいウインドウのハンドルを指定します。 戻り値は、ウインドウ全体を表すデバイスコンテキストのハンドルです。

今回のプログラムは、GetWindowDCを使ってウインドウのタイトルを描画します。 これまでのプログラムと違う話題となっているため、無理に理解する必要はありません。

#include <windows.h>

void PaintCaption(HWND hwnd, BOOL bActive);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_NCACTIVATE: {
		LRESULT lr;

		lr = DefWindowProc(hwnd, uMsg, wParam, lParam);
		PaintCaption(hwnd, (BOOL)wParam);

		return lr;
	}

	case WM_NCPAINT: {
		LRESULT lr;

		lr = DefWindowProc(hwnd, uMsg, wParam, lParam);
		PaintCaption(hwnd, hwnd == GetActiveWindow());
		
		return lr;
	}
	
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void PaintCaption(HWND hwnd, BOOL bActive)
{
	HDC      hdc;
	RECT     rcWindow, rcCaption;
	HBRUSH   hbr;
	COLORREF crText, crCaption;
	
	hdc = GetWindowDC(hwnd);
	GetWindowRect(hwnd, &rcWindow);

	rcCaption.left   = GetSystemMetrics(SM_CXFRAME) + GetSystemMetrics(SM_CXSMICON) + GetSystemMetrics(SM_CXBORDER);
	rcCaption.top    = GetSystemMetrics(SM_CYFRAME);
	rcCaption.right  = (rcWindow.right - rcWindow.left) - GetSystemMetrics(SM_CXFRAME) - (GetSystemMetrics(SM_CXSIZE) * 3);
	rcCaption.bottom = GetSystemMetrics(SM_CYCAPTION);

	crText = GetSysColor(bActive ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT);
	crCaption = GetSysColor(bActive ? COLOR_ACTIVECAPTION : COLOR_INACTIVECAPTION);
	
	hbr = CreateSolidBrush(crCaption);
	FillRect(hdc, &rcCaption, hbr);
	DeleteObject(hbr);
	
	SetTextColor(hdc, crText);
	SetBkMode(hdc, TRANSPARENT);
	DrawText(hdc, TEXT("EternalWindows"), -1, &rcCaption, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

	ReleaseDC(hwnd, hdc);
}

ウインドウの非クライアント領域(ウインドウのタイトルバーや枠)に 描画が必要となったときはWM_NCPAINTが送られます。 このメッセージが発生するタイミングはWM_PAINTと似ていますが、 WM_NCPAINTは再描画のためのメッセージではありません。 ですから、無効領域を有効にするといった概念はありません。 なお、NCとはNonClientのことです。

case WM_NCPAINT: {
	LRESULT lr;

	lr = DefWindowProc(hwnd, uMsg, wParam, lParam);
	PaintCaption(hwnd, hwnd == GetActiveWindow());
	
	return lr;
}

PaintCaptionは、タイトルを独自に描画するための自作関数です。 第2引数は、ウインドウがアクティブであるかどうかを示すフラグを受け取ります。 これは、アクティブの状態に応じて文字列の色を変えたいと思ったからです。 PaintCaptionの前にDefWindowProcを呼び出しているのは、 一度、非クライアント領域をデフォルトの状態にするためです。 WM_NCPAINTでは非クライアント領域全部の描画を賄うというよりも、 どちらかというと、デフォルトの状態に上書きするためのメッセージです。

PaintCaptionで行うことはタイトルの描画ですから、 どこからどこまでをタイトルの範囲とするのかを考えなくてはなりません。 今回のプログラムが定義する範囲は、ウインドウのアイコンの右端から 最小化ボタンが位置するところまでとします。 実際に範囲を計算するにあたって、ウインドウの幅やアイコンの大きさなどを 取得しなくてはなりません。

GetWindowRect(hwnd, &rcWindow);

GetWindowRectは、スクリーン座標におけるウインドウの位置を返します。 スクリーン座標についてはここでは説明しませんが、 (rcWindow.right - rcWindow.left) とすることによって、 ウインドウの幅が求められることは覚えておいてください。

次にすることは、ウインドウのアイコンや最小化ボタンの幅を取得し、 それらとウインドウの幅を基に、タイトルの範囲を作成することです。

rcCaption.left   = GetSystemMetrics(SM_CXFRAME) + GetSystemMetrics(SM_CXSMICON) + GetSystemMetrics(SM_CXBORDER);
rcCaption.top    = GetSystemMetrics(SM_CYFRAME);
rcCaption.right  = (rcWindow.right - rcWindow.left) - GetSystemMetrics(SM_CXFRAME) - (GetSystemMetrics(SM_CXSIZE) * 3);
rcCaption.bottom = GetSystemMetrics(SM_CYCAPTION);

GetSystemMetricsを呼び出せば、アイコンの幅などを取得することができます。 プログラムでGetSystemMetricsに指定している引数は以下のようになります。

定数 意味
SM_CX(Y)FRAME サイズ変更可能なウインドウの周囲を囲む枠の幅や高さをピクセル単位で取得する。
SM_CXSMICON 小アイコンの推奨サイズをピクセル単位で取得する。小アイコンはウインドウのタイトルバー内などで表示される。
SM_CXBORDER 立体効果のないウインドウの境界の幅を取得する。
SM_CXSIZE ウインドウのタイトルバー内のボタンの幅をピクセル単位で取得する。
SM_CYCAPTION 通常のタイトルバーの高さをピクセル単位で取得する。

上の表を参考に、一行ずつ解読していきます。

rcCaption.left = GetSystemMetrics(SM_CXFRAME) + GetSystemMetrics(SM_CXSMICON) + GetSystemMetrics(SM_CXBORDER);	

先に述べたようにタイトルの開始位置はウインドウのアイコンの右端とするので、 SM_CXSMICONでウインドウのアイコン分、足しておきます。 また、ウインドウのアイコンの左にはウインドウの枠があるので、これも足しておきます。 SM_CXBORDERは何を表しているのかがよく分かりませんが、 これを足さないとアイコンの右端を上書きしてしまうことになります。

rcCaption.top = GetSystemMetrics(SM_CYFRAME);

開始位置の垂直位置を指定します。 タイトルは枠の下から書かれているので、枠は考慮すべきです。

rcCaption.right = (rcWindow.right - rcWindow.left) - GetSystemMetrics(SM_CXFRAME) - (GetSystemMetrics(SM_CXSIZE) * 3);

ウインドウの幅からウインドウの右枠の部分を引き、最小化、最大化、 閉じるボタンの3つの幅の合計を引きます。

rcCaption.bottom = GetSystemMetrics(SM_CYCAPTION);

これは、単純にタイトルバーの高さを代入しておけばよいと思われます。

crText = GetSysColor(bActive ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT);
crCaption = GetSysColor(bActive ? COLOR_ACTIVECAPTION : COLOR_INACTIVECAPTION);

タイトルの描画に使う色を初期化します。 crTextはタイトルの文字色、crCaptionは背景色です。 GetSysColorは、定義されている値を基にシステムカラーを返します。 カラーは定数の名前から想像できると思われます。

hbr = CreateSolidBrush(crCaption);
FillRect(hdc, &rcCaption, hbr);
DeleteObject(hbr);

カラーでタイトルの範囲を塗りつぶします。 CreateSolidBrushで作成したブラシはDeleteObjectで破棄しなくてはなりません。 できれば、WM_CREATEでブラシの作成を済ましておき、 後はそれを使い続けるようにすればよいように思えますが、 ユーザーによるシステムカラーの変更を考えると、毎回作成するしかないでしょう。

SetTextColor(hdc, crText);
SetBkMode(hdc, TRANSPARENT);
DrawText(hdc, TEXT("EternalWindows"), -1, &rcCaption, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

文字列を透過描画します。 DrawTextの第5引数にDT_CENETRが設定されているため、中央に文字列が描画されることになります。

プログラムでは、WM_NCPAINTの他にWM_NCACTIVATEを処理しています。

case WM_NCACTIVATE: {
	LRESULT lr;

	lr = DefWindowProc(hwnd, uMsg, wParam, lParam);
	PaintCaption(hwnd, (BOOL)wParam);

	return lr;
}

このメッセージは、非クライアント領域のアクティブの状態が変更したときに送られ、 アクティブになったときはwParamがTRUE、非アクティブのときはFALSEとなります。 アクティブの状態が変更された瞬間にはWM_NCPAINTが送られないので このメッセージを処理する必要があるのです。

さて、今回のプログラムではウインドウのタイトルを独自に描画しているわけですが、 タイトルの色をFillRectで塗りつぶす必要はあるのでしょうか。 確かにデフォルトの色の状態でテキストのみ描画した方が見栄えはよくなりますが、 それではウインドウの本来のタイトル(今回の場合"sample")が表示されたままとなります。 CreateWindowExの第3引数をNULLにすることで、ウインドウのタイトルを空白にすればよいように思えますが、 それではタスクバー上でのアイテムの名前も空白となってしまいます。 こうした理由から、タイトル全体をFillRectで塗りつぶし、 あたかも本来のタイトルが表示されていないかのように見せかけています。


戻る