・概要
VC++ 2005の標準コントロールのうち、チェックボックスは下のダイアログ1のように白抜きに 黒というのがお決まりです。今回はそれをダイアログ2のようにチェックボックス内の背景色やチェック 自体の色を変更してみます。
ダイアログ1 | ダイアログ2 |
手法は三角ボタンの作成で使った オーナードローで処理します。つまり、チェックをした時/しない時、 フォーカス/非フォーカス時などすべての状況を考慮してプログラマ(あなた)が描くことになります。 オーナードローなんていかにもプログラミングやってるぞというような名前ですが、 実際は地味な描画作業が続いてイライラするかもしれませんが我慢してください。 作業概要は次のとおりです。
・実際
(プロジェクト作成)
それでは実際に上記ダイアログ1・2のようなダイアログベースの プロジェクトを作成します。Visual Studioを起動したファイル(F)→新規作成(N)→プロジェクト(P)で 「新しいプロジェクト」ダイアログボックスで以下のように設定してください。
プロジェクトの種類(P):Visual C++ テンプレート(T) :MFCアプリケーション プロジェクト名(N) :任意 →OK
アプリケーションウィザードが起動するので次のように設定してください。
→次へ <アプリケーションの種類> アプリケーションの種類 :ダイアログベース(D) →次へ <ユーザーインターフェイス機能> ダイアログタイトル : CheckBox Test (任意) →次へ <高度な機能> →次へ <生成されたクラス> CxxxApp、CxxxDlgの2つのクラスができていますので「完了」をクリックしてください。
(リソース作成)
表示(V)→その他のウィンドウ(E)→リソースウィンドウ(R)で リソースエディタを起動して上記ダイアログ1のように2つのチェックボックスと1つのボタンを 追加してプロパティウィンドウから各リソースIDが以下のようか確認してください。
コントロールの種類 | リソースID |
チェックボックス | IDC_CHECK1(デフォルト) |
チェックボックス | IDC_CHECK2(デフォルト) |
コマンドボタン | IDC_BUTTON1(デフォルト) |
(今回はIDC_CHECK1についてだけオーナー描画をします。)
(CButton派生クラス作成 & 実装)
VSのツールバーのプロジェクト(P)→クラスの追加(C)で 「クラスの追加」ダイアログを表示させて次のように設定して「追加(A)」をクリックしてください。
カテゴリ(C) :MFC テンプレート(T) :MFCクラス
クラスウィザードで次のように設定して「完了」をクリックしてください。
クラス名(L) :CCheckBox (任意) 基本クラス(B) :CButton
CheckBox.h、CheckBox.cppが追加されてスケルトンが できているので次のように加筆・修正してください。
//CCheckBox.h : ヘッダファイル #pragma once // CCheckBox class CCheckBox : public CButton { // ・ // ・ // ・ protected: BOOL m_bCheck; //チェック判定フラグ public: virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); //オーナードロー用 int GetCheck() const; //チェック状態取得 void SetCheck(int nCheck); //チェック状態設定 }; |
// CheckBox.cpp : 実装ファイル // // ・ // ・ // ・ //コンストラクタ CCheckBox::CCheckBox() : m_bCheck(FALSE) { } // ・ // ・ // ・ //オーナードロー用 void CCheckBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC); //描画用デバイスコンテキスト CRect rect = lpDrawItemStruct->rcItem; //描画範囲Rectangle //チェックボックス枠描画 CRect rectCheck(CPoint(0, 0), CSize(rect.Height() - 1, rect.Height() - 1)); pDC->Draw3dRect(rectCheck, RGB(10, 10, 10), RGB(250, 250, 250)); //チェックボックス内塗りつぶし CRect rectInner = rectCheck; rectInner.DeflateRect(1, 1); pDC->FillSolidRect(&rectInner, RGB(255, 196, 255)); //チェックボックス内の色 //Check/UnCheck確認 if (lpDrawItemStruct->itemState & ODS_SELECTED) //(A) { m_bCheck = !m_bCheck; } //チェック描画 if (m_bCheck) { CPen pen(PS_SOLID, 2, RGB(0, 160, 160)); //チェックの太さ/色 CPoint point[3]; CPen *pOldPen = NULL; point[0].SetPoint(rectCheck.left + 3, rectCheck.top + 4); point[1].SetPoint(rectCheck.left + 4, rectCheck.bottom - 5); point[2].SetPoint(rectCheck.right - 4, rectCheck.top + 3); pOldPen = pDC->SelectObject(&pen); pDC->Polyline(point, sizeof point/ sizeof point[0]); pDC->SelectObject(pOldPen); } //Caption描画 CString strCaption(_T("チェック")); //Caption名 int nBkMode = pDC->GetBkMode(); pDC->SetBkMode(TRANSPARENT); pDC->TextOutW(rectCheck.Height() + 1, 0, strCaption); pDC->SetBkMode(nBkMode); //Focus枠描画 CSize sizeText = pDC->GetOutputTextExtent(strCaption); CRect rectFocus(CPoint(rectCheck.Height() + 1, 0), sizeText); static BOOL bDraw = FALSE; //Focus枠描画フラグ (B) rectFocus.InflateRect(1, 0); //Focus描画補正(コントロール再描画時) if ((lpDrawItemStruct->itemState & ODS_FOCUS) && (lpDrawItemStruct->itemAction & ODA_DRAWENTIRE) && bDraw ) { static BOOL bSelect = FALSE; BOOL bSkip = FALSE; if (bSelect || lpDrawItemStruct->itemState & ODS_SELECTED) { bSelect = !bSelect; bSkip = TRUE; } if (! bSkip) pDC->DrawFocusRect(&rectFocus); } //Focus枠描画/非描画 if ( (lpDrawItemStruct->itemState & ODS_FOCUS) != bDraw ) { pDC->DrawFocusRect(&rectFocus); bDraw = lpDrawItemStruct->itemState & ODS_FOCUS; } } //チェック状態取得 int CCheckBox::GetCheck() const { return m_bCheck ? BST_CHECKED : BST_UNCHECKED; } //チェック状態設定 void CCheckBox::SetCheck(int nCheck) { switch (nCheck) { case BST_CHECKED: m_bCheck = TRUE; break; case BST_UNCHECKED: m_bCheck = FALSE; break; default: ; } Invalidate(); } |
ボタンのスタイルをBS_OWNERDRAWに設定するとシステムは何も描画してくれません。 つまり、CButton::DrawItemをオーバーライドしないとリソースビューで作ったダイアログ1の状態で 表示されません。「オン1」がのっぺらぼうになってしまいます。(注1・2参照)
そこで、描画必要になるとフレームワークからこの関数が呼ばれるので引数の DRAWITEMSTRUCT構造体のメンバを利用して描画していきます。この構造体には描画に必要な情報が 渡されているのでちょっとみてみます。
typedef struct tagDRAWITEMSTRUCT { UINT CtlType; //コントロールの種類 UINT CtlID; //コントロールID UINT itemID; //メニュー項目ID, リスト・コンボiIndex UINT itemAction; //要求される描画動作 UINT itemState; //描画後の表示状態 HWND hwndItem; //ウィンドウハンドル HDC hDC; //デバイスコンテキストハンドル RECT rcItem; //描画範囲のRectangle ULONG_PTR itemData; //コンボ・リストボックスの各項目データ内容 } DRAWITEMSTRUCT, NEAR *PDRAWITEMSTRUCT, FAR *LPDRAWITEMSTRUCT;
このうち、itemID、itemDataはチェックボックスの場合は使用しません。 この中で今回最も重要なメンバはitemStateです。このメンバには描画された後の状態が収められています。 どのような状態があるかというと、
#define ODS_SELECTED 0x0001 //選択状態 #define ODS_GRAYED 0x0002 //項目の淡色表示(メニューのみ) #define ODS_DISABLED 0x0004 //使用不可状態 #define ODS_CHECKED 0x0008 //メニューのチェック状態(メニューのみ) #define ODS_FOCUS 0x0010 //入力フォーカス取得状態
です。ODS_GRAYEDとODS_CHECKEDはメニューの時のみ使われるので、有効な値は他の3つです。
このうち、チェック/非チェック状態を確認するのはODS_SELECTEDしかないのは想像できると思います。
この値はチェックボックスの場合、マウスの左ボタンDownで設定され、ボタンUpで非設定状態で
計2回DrawItemが呼ばれます。つまり、ODS_SELECTEDの設定の有無をみてチェックを描画すると、
左ボタンDownでチェックされ、ボタンUpでチェックが外されてしまってうまくいきません。
そこで上記の(A)のようにチェック状態を保持するm_bCheckメンバ変数を用意してODS_SELECTEDが
確認されたらXORでm_bCheckの値を切り替えて、この値によってチェックの描画を判断するようにします。
itemActionメンバはどのような描画を要求しているかを表しているかで、itemStateと 組み合わせて利用されることが多いです。ただし、今回のチェックボックスの描画の場合は使用しません。 余談ですが、Windows XPと Windows 2000ではなぜか送られる値が異なるのでちょっと困惑しました。
CtlIDメンバは複数のコントロールでオーナードローを行う場合に、どのコントロールを 描画するかを認識するのに必要になります。
実際の描画はデバイスコンテキストのハンドルhDCを使って、rectItemの領域内に 描画していきます。CDC::FromHandleで一時的なCDCへのポインタを取得後、上記CCheck::DrawItemに 定義されているように、チェックボックスの描画、好みの色による内部の塗りつぶし、チェックの 有無の確認後の描画、キャプションの描画順にコーディングしていきます。
最後に入力フォーカス時の描画ですが、チェック/非チェックの確認の時と同様に itemStateメンバのODS_FOCUSの有無を確認してDrawFocusRectでフォーカスがあることを示す 破線の四角形の枠を描画します。ところがこの関数はちょっと癖があって、枠が描画されて いなければ描画しますが、描画されていてこの関数を実行すると消されてしまいます。 ということで、ODS_FOCUSで現在フォーカスがあるのかないのかを確認するだけでなく、 現在描画されているかどうかを確認するためのstaticな変数bDrawを(B)で用意しています。
最後にオーナードローにした場合CButton::GetCheck、CButton::SetCheckが 無効になるので、チェック状態を保持するm_bCheckを使って再定義します。
以上でチェックボックスの背景色とチェックをカラーで表示するクラスができたので、 ダイアログクラスでサブクラス化してリソースエディタで作成したIDC_CHECK1の チェックボックスに結び付けます。
注1.ボタンスタイルのBS_OWNERDRAWへの設定変更は この後のサブクラス化時に、ダイアログクラスCxxxxDlgで行います。
注2.実際はBS_OWNERDRAWを設定してCCheckBoxクラスで
DrawItem関数をオーバーライドしないと、CButton::DrawItemで
void CButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct){ASSERT(FALSE);}
と定義されているのでアサーションで引っかかります。
(アプリケーションクラス & ダイアログクラスへの加筆)
最初に作成したCCheckBoxクラスを使うために、アプリケーション クラスCxxxAppとダイアログクラスCxxxDlgの実装ファイルにヘッダファイルをインクルードします。
// xxxxxxx.cpp : アプリケーションクラスの実装ファイル // #include "stdafx.h" #include "xxxxxxxx.h" #include "CheckBox.h" #include "xxxxxxDlg.h" |
// xxxxDlg.cpp : ダイアログクラスの実装ファイル // #include "stdafx.h" #include "xxxxxxxx.h" #include "CheckBox.h" #include "xxxxxxDlg.h" |
最後にダイアログクラスにCCheckBoxクラスのメンバ変数を 用意して、ダイアログクラスのサブクラスに設定します。ヘッダファイルCxxxDlg.hと 実装ファイルCxxxDlg.cppに以下のように記述してください。
// xxxxxxDlg.h : ダイアログヘッダーファイル // #pragma once // CtestCheckBoxDlg ダイアログ class CtestCheckBoxDlg : public CDialog { // ・ // ・ // ・ protected: CCheckBox m_check; public: afx_msg void OnBnClickedButton1(); //チェック反転ボタンクリック }; |
// xxxxxxDlg.cpp : ダイアログ実装ファイル // #include "stdafx.h" #include "xxxxxxx.h" #include "CheckBox.h" #include "xxxxxxxDlg.h" // ・ // ・ // xxxxxxxDlg ダイアログ // ・ // ・ BEGIN_MESSAGE_MAP(CtestCheckBoxDlg, CDialog) ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_BN_CLICKED(IDC_BUTTON1, &CtestCheckBoxDlg::OnBnClickedButton1) END_MESSAGE_MAP() // CtestCheckBoxDlg メッセージハンドラ BOOL CtestCheckBoxDlg::OnInitDialog() { CDialog::OnInitDialog(); // ・ // ・ // ・ //チェックボックス(IDC_CHECK1)サブクラス化 m_check.SubclassDlgItem(IDC_CHECK1, this); //CCheckBox::DrawItemが呼ばれるようスタイル変更 m_check.ModifyStyle(0, BS_OWNERDRAW); return TRUE; } // ・ // ・ // ・ //チェック反転ボタンクリック void CtestCheckBoxDlg::OnBnClickedButton1() { m_check.SetCheck(m_check.GetCheck() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED); LPCTSTR pszText = (m_check.GetCheck() == BST_CHECKED ? _T("") : _T("Un")); TRACE(_T("CheckBox(IDC_CHECK1) was %sChecked\n"), pszText); } |
ダイアログボックスクラスへの加筆は数行です。ヘッダファイルにCCheckBoxクラスの メンバm_checkを宣言して、CxxxDlg::OnInitDialogでサブクラス化をした後、DrawItemが呼ばれるように ボタンスタイルにBS_OWNERDRAWを加えます。
以上で最初に掲載したダイアログ2のようにカラフルなチェックボックスができている はずです。あとは蛇足っぽいですが、CCheckBox::GetCheck、CCheckBox::SetCheckの動作確認で チェック反転ボタン(IDC_BUTTON1)の実装を上記のようにしてください。