第261章 カスタムドロー(リストビュー)


オーナードローについてはすでに第41章(ボタン)、 第45章(メニュー)、 第60章(ステータスバー)、第86章(ボタンの変形)などいろいろな所で 出てきました。オーナードローするときはオーナーが何から何まで 全部やらなくてはならず、結構面倒くさいでした。カスタムドローは、 オーナードローと似ていますが、自分でやりたいところだけ(?)をやればよいので 少し楽です。

左の図はリストビューをカスタムドローした例です。メニューの「オプション」 「カスタムドロー」が選択されていると、チェックのついた所だけ フォント、背景色、文字色をカスタムドローしています。チェックをはずすと 元のフォント、背景色、文字色に戻ります。メニューの「カスタムドロー」が 選択されていないと、チェックを付けても変化は起こりません。



コモンコントロールをカスタムドローするには NM_CUSTOMDRAW通知メッセージをを処理する必要があります。 例によってNM_CUSTOMDRAWはWM_NOTIFYメッセージの形でやってきます。

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

NM_CUSTOMDRAW通知メッセージが来たときのlParamはコモンコントロールの種類に よって特有の構造体へのアドレスとなります。今回使用するのはリストビューなので NMLVCUSTOMDRAW構造体についてみてみると、

typedef struct tagNMLVCUSTOMDRAW { NMCUSTOMDRAW nmcd; COLORREF clrText; COLORREF clrTextBk; #if (_WIN32_IE >= 0x0400) int iSubItem; #endif } NMLVCUSTOMDRAW, *LPNMLVCUSTOMDRAW;

のように定義されています。どの構造体でも最初のメンバはNMCUSTOMDRAW 構造体となっています。

nmcdはNMCUSTOMDRAW構造体です。

clrTextはリストビューコントロールのテキスト色です。

clrTextBkは背景色です。

iSubItemは、は描画しようとしているサブアイテムのIDです。(comctl32.dllがVer.4.71以降)

NMCUSTOMDRAW構造体

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

のように定義されています。(Ver.4.70以降)

hdrはNMHDR構造体です。

dwDrawStageは現在の描画ステージを表しています。描画ステージは 次のいずれかです。

グローバル描画ステージ
CDDS_POSTERASE消去サイクルが完了後
CDDS_POSTPAINT描画サイクル完了後
CDDS_PREERASE消去サイクルが始まる前
CDDS_PREPAINT描画サイクルが始まる前
アイテム特有描画ステージ
CDDS_ITEMdwItemSpec, uItemState, lItemlParamメンバが
有効であることを指し示す
CDDS_ITEMPOSTERASEアイテムが消去された後
CDDS_ITEMPOSTPAINTアイテムが描画された後
CDDS_ITEMPREERASEアイテムが消去される前
CDDS_ITEMPREPAINTアイテムが描画される前
CDDS_SUBITEMVer.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

アイテム追加用ダイアログボックスの実際の外観は左のような感じになります。



// 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;

コモンコントロールの前準備(commctrl.hのインクルード、comctl32.libのリンクなど) を忘れないでください。

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; }

この辺はいつもと同じです。

//ウィンドウプロシージャ 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; }

WM_CREATEメッセージが来たら、InitCommonControlsEx関数でコモンコントロールの 初期化をします。そして、自作関数MakeMyListを読んでリストビューを作ります。 それと、カスタムドローをしないときのフォント(system)をWM_SETFONTメッセージで セットしておきます。カスタムドローした時に文字がはみ出さないようにするための 算段です。フォントの高さをhOrgFontとhCustomFontで同じにしてあります。 WM_SETFONTメッセージの解説は第194章を参照してください。

メニューで「書き込み」(IDM_WRITE)が選択されたら書き込み用ダイアログボックスを 出します。ダイアログボックスにリストビューのハンドルを渡すために DialogBoxマクロではなくDialogBoxParam関数を呼んでいます。この関数については 第195章を参照してください。

メニューから「カスタムドロー」(IDM_CUSTOM)が選択されたら、bCustomがFALSEなら チェックをつけて、bCustomをTRUEにします。bCustomがTRUEならチェックをはずして bCustomをFALSEにします。そして、リストビューを再描画させます。 MENUITEMINFO構造体については第179章を参照してください。

WM_NOTIFYメッセージの処理は最初の方の解説を読んでいただければわかると思います。

WM_SIZEメッセージが来たらリストビューの位置、大きさを調整します。

プログラム終了時にCreateFont関数で作ったフォントハンドルをDeleteObjectするのを 忘れないでください。

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; }

リストビューを作る関数です。新しいウィンドウスタイルについては 第198章を参照してください。

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関数を呼んでダイアログボックス を閉じます。

カスタムドローは慣れれば、オーナードローよりずっと簡単なはず(?)です。


[SDK第3部 Index] [総合Index] [Previous Chapter] [Next Chapter]

Update 20/Mar/2000 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。