コモンコントロールをカスタムドローするには
NM_CUSTOMDRAW通知メッセージをを処理する必要があります。
例によってNM_CUSTOMDRAWはWM_NOTIFYメッセージの形でやってきます。
NM_CUSTOMDRAW通知メッセージが来たときのlParamはコモンコントロールの種類に よって特有の構造体へのアドレスとなります。今回使用するのはリストビューなので NMLVCUSTOMDRAW構造体についてみてみると、NM_CUSTOMDRAW #ifdef LIST_VIEW_CUSTOM_DRAW lpNMCustomDraw = (LPNMLVCUSTOMDRAW) lParam; #elif TOOL_TIPS_CUSTOM_DRAW lpNMCustomDraw = (LPNMTTCUSTOMDRAW) lParam; #elif TREE_VIEW_CUSTOM_DRAW lpNMCustomDraw = (LPNMTVCUSTOMDRAW) lParam; #elif TOOL_BAR_CUSTOM_DRAW lpNMCustomDraw = (LPNMTBCUSTOMDRAW) lParam; #else lpNMCustomDraw = (LPNMCUSTOMDRAW) lParam; #endif
のように定義されています。どの構造体でも最初のメンバはNMCUSTOMDRAW 構造体となっています。typedef struct tagNMLVCUSTOMDRAW { NMCUSTOMDRAW nmcd; COLORREF clrText; COLORREF clrTextBk; #if (_WIN32_IE >= 0x0400) int iSubItem; #endif } NMLVCUSTOMDRAW, *LPNMLVCUSTOMDRAW;
nmcdはNMCUSTOMDRAW構造体です。
clrTextはリストビューコントロールのテキスト色です。
clrTextBkは背景色です。
iSubItemは、は描画しようとしているサブアイテムのIDです。(comctl32.dllがVer.4.71以降)
NMCUSTOMDRAW構造体は
のように定義されています。(Ver.4.70以降)typedef struct tagNMCUSTOMDRAWINFO { NMHDR hdr; DWORD dwDrawStage; HDC hdc; RECT rc; DWORD dwItemSpec; UINT uItemState; LPARAM lItemlParam; } NMCUSTOMDRAW, FAR * LPNMCUSTOMDRAW;
hdrはNMHDR構造体です。
dwDrawStageは現在の描画ステージを表しています。描画ステージは 次のいずれかです。
グローバル描画ステージ | |
---|---|
CDDS_POSTERASE | 消去サイクルが完了後 |
CDDS_POSTPAINT | 描画サイクル完了後 |
CDDS_PREERASE | 消去サイクルが始まる前 |
CDDS_PREPAINT | 描画サイクルが始まる前 |
アイテム特有描画ステージ | |
CDDS_ITEM | dwItemSpec, uItemState, lItemlParamメンバが 有効であることを指し示す |
CDDS_ITEMPOSTERASE | アイテムが消去された後 |
CDDS_ITEMPOSTPAINT | アイテムが描画された後 |
CDDS_ITEMPREERASE | アイテムが消去される前 |
CDDS_ITEMPREPAINT | アイテムが描画される前 |
CDDS_SUBITEM | Ver.4.71以降で有効 サブアイテムが描画されようとしているときCDDS_ITEMPREPAINTまたはCDDS_ITEMPOSTPAINTに 結合したフラグ。 CDDS_PREPAINTからCDRF_NOTIFYSUBITEMDRAWが返されたときのみセットされます。 |
hdcは、コントロールのデバイスコンテキストハンドルです。
rcは描画されようとしている領域のRECT構造体です。
dwItemSpecはアイテム番号です。通知を送ってくるコントロールの種類によって意味が異なります。
uItemStateは、現在のアイテムの状態を示します。次の組み合わせで表します。
CDIS_CHECKED | アイテムはチェックされています。 |
CDIS_DEFAULT | アイテムはデフォルト状態です。 |
CDIS_DISABLED | アイテムは使用不可になっています。 |
CDIS_FOCUS | アイテムはフォーカスされています。 |
CDIS_GRAYED | アイテムは灰色表示になっています。 |
CDIS_HOT | アイテムはhot状態になっています。 |
CDIS_INDETERMINATE | アイテムはinterminate状態になっています。 |
CDIS_MARKED | アイテムはマークされています。 |
CDIS_SELECTED | アイテムは選択されています。 |
lItemlParamはアプリケーション定義のデータです。
さて、コントロールはそれぞれの描画サイクルの始まりに当たってNM_CUSTOMDRAW通知メッセージ を送ってきますが、どのステージかはNMCUSTOMDRAW構造体のdwDrawStageメンバを調べればわかります。 アプリケーションは最初の通知を検出したら、残りの描画サイクルのカスタムドロー通知をいつ、どのように 受けるかをreturnしなくてはいけません。
戻り値 | 効果 |
---|---|
CDRF_DODEFAULT | コントロールは自分自身で描画します。この描画サイクルでは もうNM_CUSTOMDRAW通知メッセージは送られません。このフラグ単独で使用します。 |
CDRF_NOTIFYITEMDRAW | コントロールはNM_CUSTOMDRAW通知メッセージを アイテム描画の前後で送ってきます。 |
CDRF_NOTIFYPOSTPAINT | コントロールは描画サイクルが完了したらWM_CUSTOMDRAW を送ってきます。 |
CDRF_SKIPDEFAULT | コントロールは全く描画をしません。 |
では、具体的にリストビューで背景色、文字色、フォントをカスタムドローする方法を 見てみましょう。
他のカスタムドローもたいていはこの手順で大丈夫です。こうやって書くと簡単そうですが 実際にプログラムを書くと結構面倒くさいです。1.普通通りリストビューを作る 2.WM_CUSTOMDRAW通知メッセージを捕まえる 3.描画ステージがCDDS_PREPAINTならば、CDRF_NOTIFYITEMDRAWを返します。 4.描画ステージがCDDS_ITEMPREPAINTならば、文字色、背景色、フォントを変更します。 5.フォントを変更したらCDRF_NEWFONTを返します。
さて、上の手順でプログラムを書くとアイテムがすべてカスタムドローされてしまいます。 そこで、チェックのついたものだけカスタムドローするにはどうしたらよいのでしょうか。 ListView_GetCheckStateマクロでチェックがつていればカスタムドローをするようにします。 この時アイテム番号はdwItemSpecメンバでわかります。
では、プログラムを見てみましょう。
アイテム追加用ダイアログボックスの実際の外観は左のような感じになります。// custom01.rcの一部 ///////////////////////////////////////////////////////////////////////////// // // Menu // MYMENU MENU DISCARDABLE BEGIN POPUP "ファイル(&F)" BEGIN MENUITEM "書き込み(&W)", IDM_WRITE MENUITEM SEPARATOR MENUITEM "終了(&X)", IDM_END END POPUP "オプション(&O)" BEGIN MENUITEM "カスタムドロー(&C)", IDM_CUSTOM END END ///////////////////////////////////////////////////////////////////////////// // // Dialog // MYDLG DIALOG DISCARDABLE 0, 0, 187, 93 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "入力用ダイアログ" FONT 9, "MS Pゴシック" BEGIN EDITTEXT IDC_EDIT1,64,7,116,17,ES_AUTOHSCROLL CONTROL "男",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,66,29,23,10 CONTROL "女",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,95,29,23,10 EDITTEXT IDC_EDIT2,64,45,116,17,ES_AUTOHSCROLL DEFPUSHBUTTON "追加書き込み",IDOK,7,72,50,14 PUSHBUTTON "閉じる",IDCANCEL,130,72,50,14 LTEXT "氏名",IDC_STATIC,22,13,15,8 LTEXT "性別",IDC_STATIC,23,33,15,8 LTEXT "住所",IDC_STATIC,23,50,15,8 END
コモンコントロールの前準備(commctrl.hのインクルード、comctl32.libのリンクなど) を忘れないでください。// custom01.cpp #ifndef STRICT #define STRICT #endif #include <windows.h> #include <windowsx.h> #include <commctrl.h> #include "resource.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM); ATOM InitApp(HINSTANCE); BOOL InitInstance(HINSTANCE, int); HWND MakeMyList(HWND); void InsertMyColumn(HWND); char szClassName[] = "custom01"; //ウィンドウクラス BOOL bCustom = FALSE;
この辺はいつもと同じです。int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } //ウィンドウ・クラスの登録 ATOM InitApp(HINSTANCE hInst) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; //プロシージャ名 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst;//インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = "MYMENU"; //メニュー名 wc.lpszClassName = (LPCSTR)szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); return (RegisterClassEx(&wc)); } //ウィンドウの生成 BOOL InitInstance(HINSTANCE hInst, int nCmdShow) { HWND hWnd; hWnd = CreateWindow(szClassName, "猫でもわかるカスタムドロー", //タイトルバーにこの名前が表示されます WS_OVERLAPPEDWINDOW, //ウィンドウの種類 CW_USEDEFAULT, //X座標 CW_USEDEFAULT, //Y座標 CW_USEDEFAULT, //幅 CW_USEDEFAULT, //高さ NULL, //親ウィンドウのハンドル、親を作るときはNULL NULL, //メニューハンドル、クラスメニューを使うときはNULL hInst, //インスタンスハンドル NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; }
WM_CREATEメッセージが来たら、InitCommonControlsEx関数でコモンコントロールの 初期化をします。そして、自作関数MakeMyListを読んでリストビューを作ります。 それと、カスタムドローをしないときのフォント(system)をWM_SETFONTメッセージで セットしておきます。カスタムドローした時に文字がはみ出さないようにするための 算段です。フォントの高さをhOrgFontとhCustomFontで同じにしてあります。 WM_SETFONTメッセージの解説は第194章を参照してください。//ウィンドウプロシージャ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { int id; static HWND hList; HINSTANCE hInst; INITCOMMONCONTROLSEX ic; LPNMHDR lpnmhdr; LPNMLISTVIEW lplv; LPNMLVCUSTOMDRAW lplvcd; MENUITEMINFO mii; static HMENU hMenu; static HFONT hCustomFont, hOrgFont; switch (msg) { case WM_CREATE: ic.dwICC = ICC_LISTVIEW_CLASSES; ic.dwSize = sizeof(INITCOMMONCONTROLSEX); InitCommonControlsEx(&ic); hList = MakeMyList(hWnd); InsertMyColumn(hList); hMenu = GetMenu(hWnd); hCustomFont = CreateFont(18, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0, SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, "DF勘亭流"); hOrgFont = CreateFont(18, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0, SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, "system"); SendMessage(hList, WM_SETFONT, (WPARAM)hOrgFont, (LPARAM)MAKELPARAM(FALSE, 0)); break; case WM_COMMAND: switch (LOWORD(wp)) { case IDM_END: SendMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_WRITE: hInst = (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE); DialogBoxParam(hInst, "MYDLG", hWnd, (DLGPROC)MyDlgProc, (LPARAM)hList); break; case IDM_CUSTOM: memset(&mii, 0, sizeof(MENUITEMINFO)); mii.cbSize = sizeof(MENUITEMINFO); mii.fMask = MIIM_STATE; if (bCustom == FALSE) mii.fState = MFS_CHECKED; else mii.fState = MFS_UNCHECKED; SetMenuItemInfo(hMenu, IDM_CUSTOM, FALSE, &mii); bCustom = !bCustom; InvalidateRect(hList, NULL, TRUE); break; } break; case WM_NOTIFY: lpnmhdr = (LPNMHDR)lp; if (lpnmhdr->hwndFrom == hList && bCustom) { lplv = (LPNMLISTVIEW)lp; if(lplv->hdr.code == NM_CUSTOMDRAW) { lplvcd = (LPNMLVCUSTOMDRAW)lp; if (lplvcd->nmcd.dwDrawStage == CDDS_PREPAINT) return CDRF_NOTIFYITEMDRAW; if (lplvcd->nmcd.dwDrawStage == CDDS_ITEMPREPAINT) { if (ListView_GetCheckState(hList, lplvcd->nmcd.dwItemSpec)) { SelectObject(lplvcd->nmcd.hdc, hCustomFont); lplvcd->clrTextBk = RGB(255, 255, 0); lplvcd->clrText = RGB(0, 0, 255); return CDRF_NEWFONT; } } break; } } break; case WM_SIZE: MoveWindow(hList, 0, 0, LOWORD(lp), HIWORD(lp), TRUE); break; case WM_CLOSE: id = MessageBox(hWnd, "終了してもよいですか", "終了確認", MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { DeleteObject(hOrgFont); DeleteObject(hCustomFont); DestroyWindow(hList); DestroyWindow(hWnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0; }
メニューで「書き込み」(IDM_WRITE)が選択されたら書き込み用ダイアログボックスを 出します。ダイアログボックスにリストビューのハンドルを渡すために DialogBoxマクロではなくDialogBoxParam関数を呼んでいます。この関数については 第195章を参照してください。
メニューから「カスタムドロー」(IDM_CUSTOM)が選択されたら、bCustomがFALSEなら チェックをつけて、bCustomをTRUEにします。bCustomがTRUEならチェックをはずして bCustomをFALSEにします。そして、リストビューを再描画させます。 MENUITEMINFO構造体については第179章を参照してください。
WM_NOTIFYメッセージの処理は最初の方の解説を読んでいただければわかると思います。
WM_SIZEメッセージが来たらリストビューの位置、大きさを調整します。
プログラム終了時にCreateFont関数で作ったフォントハンドルをDeleteObjectするのを 忘れないでください。
リストビューを作る関数です。新しいウィンドウスタイルについては 第198章を参照してください。HWND MakeMyList(HWND hWnd) { HWND hList; DWORD dwStyle; HINSTANCE hInst; hInst = (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE); hList = CreateWindowEx(0, WC_LISTVIEW, "", WS_CHILD | WS_VISIBLE | LVS_REPORT, 0, 0, 0, 0, hWnd, (HMENU)100, hInst, NULL); dwStyle = ListView_GetExtendedListViewStyle(hList); dwStyle |= LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_HEADERDRAGDROP; ListView_SetExtendedListViewStyle(hList, dwStyle); return hList; }
リストビューに列を追加する関数です。void InsertMyColumn(HWND hList) { LVCOLUMN lvcol; lvcol.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; lvcol.fmt = LVCFMT_LEFT; lvcol.cx = 100; lvcol.pszText = "名前"; lvcol.iSubItem = 0; ListView_InsertColumn(hList, 0, &lvcol); lvcol.cx = 50; lvcol.pszText = "性別"; lvcol.iSubItem = 1; ListView_InsertColumn(hList, 1, &lvcol); lvcol.cx = 200; lvcol.pszText = "住所"; lvcol.iSubItem = 2; ListView_InsertColumn(hList, 2, &lvcol); return; }
書き込み用ダイアログボックスのプロシージャです。LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp) { static HWND hList; int nCount; LVITEM li; char szName[32], szAddress[256]; switch (msg) { case WM_INITDIALOG: hList = (HWND)lp; Button_SetCheck(GetDlgItem(hDlg, IDC_RADIO1), BST_CHECKED); return TRUE; case WM_COMMAND: switch (LOWORD(wp)) { case IDOK: nCount = ListView_GetItemCount(hList); memset(&li, 0, sizeof(LVITEM)); li.mask = LVIF_TEXT; li.iItem = nCount; li.iSubItem = 0; Edit_GetText(GetDlgItem(hDlg, IDC_EDIT1), szName, sizeof(szName)); li.pszText = szName; ListView_InsertItem(hList, &li); li.iSubItem = 1; if (Button_GetCheck(GetDlgItem(hDlg, IDC_RADIO1)) == BST_CHECKED) li.pszText = "男"; if (Button_GetCheck(GetDlgItem(hDlg, IDC_RADIO2)) == BST_CHECKED) li.pszText = "女"; ListView_SetItem(hList, &li); li.iSubItem = 2; Edit_GetText(GetDlgItem(hDlg, IDC_EDIT2), szAddress, sizeof(szAddress)); li.pszText = szAddress; ListView_SetItem(hList, &li); Edit_SetText(GetDlgItem(hDlg, IDC_EDIT1), ""); Edit_SetText(GetDlgItem(hDlg, IDC_EDIT2), ""); SetFocus(GetDlgItem(hDlg, IDC_EDIT1)); return TRUE; case IDCANCEL: EndDialog(hDlg, IDCANCEL); return TRUE; } return FALSE; } return FALSE; }
WM_INITDIALOGメッセージが来たときにlParamでDialogBoxParam関数の最後の引数 を受け取ることができます。
「追加書き込みボタン」(IDOK)が押されたらリストビューにアイテムを挿入します。 この時、連続して書き込みができるようにEndDialog関数は呼んでいません。
「閉じるボタン」(IDCANCEL)が押されたら、EndDialog関数を呼んでダイアログボックス を閉じます。
カスタムドローは慣れれば、オーナードローよりずっと簡単なはず(?)です。
Update 20/Mar/2000 By Y.Kumei