EternalWindows
コントロール基礎 / オーナードロー

コントロールは、アプリケーションの操作性を支援する一方で、 アプリケーションの見た目を構成する重要な要素でもあります。 このため、ときにはコントロール自身の外観をカスタマイズし、 ユーザーに見栄え良く見せるための工夫が必要になることがあります。 オーナードローとは、コントロール(またはメニュー項目)の外観をアプリケーションが一から描画することであり、 こうした独自の描画を行ってもコントロールの本来の機能(WM_COMMANDの生成など)が失われないという利点があります。

ボタンに対してオーナードローを行うには、ウインドウスタイルにBS_OWNERDRAWを指定することになります。 この定数を指定した場合、ボタンは背景色だけが描画され、ボタンのテキストや輪郭などは描画されません。 理由は、これらを描画するのがアプリケーションの役割になるからです。 アプリケーションは、オーナードローのタイミングを示すWM_DRAWITEMを検出し、 そこでボタンのデバイスコンテキストのハンドルを取得することによって、 実際に描画を行うことになります。 WM_DRAWITEMのwParamは、オーナードローの対象であるコントロールのIDであり、 lParamはDRAWITEMSTRUCT構造体のアドレスが格納されます。

typedef struct tagDRAWITEMSTRUCT {
  UINT CtlType;
  UINT CtlID;
  UINT itemID;
  UINT itemAction;
  UINT itemState;
  HWND hwndItem;
  HDC hDC;
  RECT rcItem;
  ULONG_PTR itemData;
} DRAWITEMSTRUCT;

CtlTypeは、コントロールのタイプを示す定数が格納されます。 CtlIDは、コントロールのIDが格納されます。 itemIDは、アイテムのIDやインデックスが格納されます。 アイテムは、リストボックスやコンボボックスにおける項目のことですが、 ボタンの場合はボタンそのものであると考えて問題ありません。 itemActionは、アイテムに要求されたアクションを示す定数が格納されます。 itemStateは、アイテムの現在の状態を示す定数が格納されます。 hwndItemは、コントロールのウインドウハンドルが格納されます。 hDCは、コントロールのデバイスコンテキストのハンドルが格納されます。 rcItemは、アイテムの長方形の座標が格納されます。 itemDataは、アプリケーション定義値が格納されます。

オーナードローでは、アプリケーションが独自に描画を行うようになるわけですが、 この描画の仕方は状況によって変化することになります。 たとえば、ユーザーがボタンを選択している場合は、 既定のボタンのように窪みを描画すべきですから、 そのような関数が必要になります。 これには、DrawEdgeを呼び出します。

BOOL DrawEdge(
  HDC hdc,
  LPRECT qrc,
  UINT edge,
  UINT grfFlags
);

hdcは、デバイスコンテキストのハンドルを指定します。 qlprcは、RECT構造体のアドレスを指定します。 edgeは、長方形の辺の内側と外側をどのように描画するかを示す定数を指定します。 grfFlagsは、境界をどのように描画するかを示す定数を指定します。

既定のボタンでは、キーボードフォーカスが割り当てられている場合に、点線の枠が表示されていることがあります。 これを描画するには、DrawFocusRectを呼び出します。

BOOL DrawFocusRect(
  HDC hDC,
  const RECT *lprc
);

hDCは、デバイスコンテキストのハンドルを指定します。 lprcは、RECT構造体のアドレスを指定します。

今回のプログラムは、ボタンを2つ作成します。 片方のボタンは、オーナードローによって独自に描画されています。

#include <windows.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 | BS_OWNERDRAW, 30, 80, 80, 40, hwnd, (HMENU)ID_BUTTON2, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		return 0;
	
	case WM_DRAWITEM: {
		TCHAR            szBuf[256];
		LPDRAWITEMSTRUCT lpDraw = (LPDRAWITEMSTRUCT)lParam;

		if (lpDraw->itemState & ODS_SELECTED)
			DrawEdge(lpDraw->hDC, &lpDraw->rcItem, EDGE_SUNKEN, BF_RECT);
		else
			DrawEdge(lpDraw->hDC, &lpDraw->rcItem, EDGE_RAISED, BF_RECT);

		if (lpDraw->itemState & ODS_FOCUS) {
			InflateRect(&lpDraw->rcItem, -4, -4);
			DrawFocusRect(lpDraw->hDC, &lpDraw->rcItem);
		}

		GetWindowText(lpDraw->hwndItem, szBuf, sizeof(szBuf) / sizeof(TCHAR));
		SetTextColor(lpDraw->hDC, RGB(255, 0, 0));
		SetBkMode(lpDraw->hDC, TRANSPARENT);
		DrawText(lpDraw->hDC, szBuf, -1, &lpDraw->rcItem, DT_CENTER | DT_VCENTER | DT_SINGLELINE);

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

WM_DRAWITEMが送られた時点では、ボタンの背景色が描画されているだけですから、 まずは辺を描画するためにDrawEdgeを呼び出しています。 itemStateにODS_SELECTEDが含まれている場合は、ボタンが選択されていることを意味し、 この場合はボタンの内側と外側を陥没させる必要があるので、EDGE_SUNKENを指定しています。 一方、選択されていない場合は、ボタンの内側と外側を隆起させるためにEDGE_RAISEDを指定しています。 第4引数は、上下左右の辺を描画することを意味するBF_RECTを指定しています。 itemStateにODS_FOCUSが含まれている場合は、ボタンにキーボードフォーカスが割り当てられていることを意味します。 この場合は、DrawFocusRectで点線の枠を描画することになりますが、 少し内側に描画するために、InflateRectで長方形を縮小しています。 ボタンテキストの描画は、DrawTextで行っています。 必要なテキストは、ボタンのウインドウハンドルをGetWindowTextに指定することで取得できます。 SetTextColorは描画時におけるテキストの色を変更し、SetBkModeはテキストを透過描画できるようにします。

今回のようなオーナードローの方法は、常にボタンがクラシックに描画されるという問題があります。 ユーザーがテーマをクラシックに設定している場合はそれでもよいのですが、 クラシックでないテーマが設定されている場合は、 テーマを反映したボタンが描画されることをユーザーは望んでいるはずです。 よって、本来ならばアプリケーションは、テーマが有効になっているかを調べ、 有効になっている場合はテーマを反映した描画を行う必要があるといえます。 この描画は、OpenThemeDataを筆頭するTheme APIを呼び出すことで実現できますが、 今回のように単にボタンのテキストの色を変更するだけであれば、 次節で取り上げるカスタムドローを利用した方がよいでしょう。

フォントの設定

コントロールのフォントを独自のものにしたい場合は、 オーナードローでデバイスコンテキストにフォントを選択するよりも、 WM_SETFONTでフォントを設定した方が遥かに簡単といえます。 次にコード例を示します。

SendMessage(GetDlgItem(hwnd, ID_BUTTON1), WM_SETFONT, (WPARAM)GetStockObject(ANSI_FIXED_FONT), 0);

WM_SETFONTのwParamには、設定したいフォントのハンドルを指定します。 上記では、GetStockObjectで取得した既定のフォントを指定していますが、 当然ながらCreateFontで作成した独自のフォントも指定できます。 上記コードをCreateWindowExの後に実行すれば、 フォントが反映した状態でボタンが表示されることになります。



戻る