EternalWindows
コントロール基礎 / カスタムドロー

カスタムドローとは、前節で取り上げたオーナードローと同じように、 アプリケーションがコントロールを独自に描画することを意味します。 ただし、カスタムドローはオーナードローのように一から描画するわけではなく、 基本的な部分については既定の描画が行われることになっています。 たとえば、ボタンなら選択時の窪みは既定で描画されることになりますから、 本当に独自の描画を行いたい部分のみに集中することができます。 なお、ボタンでカスタムドローを行うには、 アプリケーションにマニフェストファイルを追加していなければならないため、 事前に次節で示す作業を行っておいてください。

カスタムドローを行うタイミングは、WM_NOTIFYを通じて送られます。 WM_NOTIFYは、WM_COMMANDと同じく通知メッセージであり、 カスタムドロー以外のいくつかの通知もこのメッセージで行われます。 WM_NOTIFYが送られた理由を特定するには、LPARAMを通じてNMHDR構造体を確認する必要があります。

typedef struct tagNMHDR {
  HWND hwndFrom;
  UINT_PTR idFrom;
  UINT code;
} NMHDR;

hwndFromは、通知の対象であるコントロールのウインドウハンドルが格納されます。 dFromは、通知の対象であるコントロールのIDが格納されます。 codeは、通知コードが格納されます。

codeにNM_CUSTOMDRAWが格納されている場合、lParamにはNMCUSTOMDRAW構造体へのアドレスが格納されています。

typedef struct tagNMCUSTOMDRAWINFO {
  NMHDR hdr;
  DWORD dwDrawStage;
  HDC hdc;
  RECT rc;
  DWORD_PTR dwItemSpec;
  UINT uItemState;
  LPARAM lItemlParam;
} NMCUSTOMDRAW, *LPNMCUSTOMDRAW;

hdrは、NMHDR構造体が格納されます。 このことから、NMCUSTOMDRAW構造体をNMHDR構造体で表すこともできます。 dwDrawStageは、描画ステージを表す定数が格納されます。 hdcは、コントロールのデバイスコンテキストのハンドルが格納されます。 このデバイスコンテキストのハンドルを通じて描画を行うことになります。 rcは、コントロールの長方形の座標が格納されます。 dwItemSpecは、アイテムの番号が格納されます。 アイテムは、コントロールによって意味が異なります。 uItemStateは、アイテムの現在の状態を表す定数が格納されます。 lItemlParamは、アプリケーション定義値が格納されます。

描画ステージは、どの段階で描画を行うかを表します。 カスタムドローでは、アプリケーションが独自の描画を行えるわけですが、 それを既定の描画の前に行うか、既定の描画の後に行うかを決定することができます。 次に、既定の描画の後に独自の描画を行う例を示します。

case WM_NOTIFY:
	if (((LPNMHDR)lParam)->code == NM_CUSTOMDRAW) {
		LPNMCUSTOMDRAW lpCustomDraw = (LPNMCUSTOMDRAW)lParam;

		if (lpCustomDraw->dwDrawStage == CDDS_PREPAINT)
			return CDRF_NOTIFYPOSTPAINT;
		else if (lpCustomDraw->dwDrawStage == CDDS_POSTPAINT) {
			lpCustomDraw->rc.right /= 2;
			FillRect(lpCustomDraw->hdc, &lpCustomDraw->rc, (HBRUSH)GetStockObject(GRAY_BRUSH));
			return 0;
		}
		else
			;
	}
	break;

WM_NOTIFYでNM_CUSTOMDRAWを検出したら、lParamをNMCUSTOMDRAW構造体で表すことができます。 dwDrawStageがCDDS_PREPAINTの場合は、既定の描画が行われる前の段階であることを意味し、 この時点ではまだ描画が行われていません。 CDDS_PREPAINTで、CDRF_NOTIFYPOSTPAINTを返した場合は、 既定の描画が行われた後にCDDS_POSTPAINTが送られることになり、 ここではボタンの左半分をFillRectで塗りつぶしています。 これは、既定の描画に対する上書きになりますから、 右半分は既定の描画結果が残ったままとなります。 CDDS_PREPAINTで0を返したりbreakをしたりする場合は、 既定の描画が行われるだけでCDDS_POSTPAINTは送られません。

今回のプログラムは、カスタムドローの既定の描画の前に独自の描画を行います。 NMCUSTOMDRAW構造体などを使用する場合は、commctrl.hのインクルードが必要になります。

#include <windows.h>
#include <commctrl.h>

#define ID_BUTTON1 100
#define ID_BUTTON2 200

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_CREATE:
		CreateWindowEx(0, TEXT("BUTTON"), TEXT("ボタン"), WS_CHILD | WS_VISIBLE, 30, 30, 80, 40, hwnd, (HMENU)ID_BUTTON1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		CreateWindowEx(0, TEXT("BUTTON"), TEXT("ボタン"), WS_CHILD | WS_VISIBLE, 30, 80, 80, 40, hwnd, (HMENU)ID_BUTTON2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		return 0;

	case WM_NOTIFY:
		if (((LPNMHDR)lParam)->idFrom == ID_BUTTON2 && ((LPNMHDR)lParam)->code == NM_CUSTOMDRAW) {
			TCHAR          szBuf[256];
			LPNMCUSTOMDRAW lpCustomDraw = (LPNMCUSTOMDRAW)lParam;

			if (lpCustomDraw->dwDrawStage == CDDS_PREPAINT){
				GetWindowText(lpCustomDraw->hdr.hwndFrom, szBuf, sizeof(szBuf) / sizeof(TCHAR));
				SetTextColor(lpCustomDraw->hdc, RGB(255, 0, 0));
				SetBkMode(lpCustomDraw->hdc, TRANSPARENT);
				DrawText(lpCustomDraw->hdc, szBuf, -1, &lpCustomDraw->rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
				return CDRF_SKIPDEFAULT;
			}
		}
		break;	

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

既定の描画が行われる前に独自の描画を行うには、 CDDS_PREPAINT内に描画コードを記述し、 CDRF_SKIPDEFAULTを返すようにします。 この値を返さないと、既定の描画が行われて既に描画した内容が上書きされてしまいますから、 CDRF_SKIPDEFAULTを返して既定の描画をスキップするのです。

ボタンにおけるカスタムドローでは、ボタン自体は既に描画されています。 これは、CDDS_PREPAINTが送られている時点で描画されているので、 アプリケーションが行うのはボタンの上に何かを描画することになります。 今回は、ボタンのテキストを赤色で描画するということで、 GetWindowTextでボタン名を取得し、次にSetTextColorでデバイスコンテキストの属性を変更、 そしてDrawTextでテキストを描画しています。


戻る