◆チェックボックスの装飾 - VC++ 2005 MFC

[ ホーム ] [ 上へ ]

・概要

VC++ 2005の標準コントロールのうち、チェックボックスは下のダイアログ1のように白抜きに 黒というのがお決まりです。今回はそれをダイアログ2のようにチェックボックス内の背景色やチェック 自体の色を変更してみます。

ダイアログ1 ダイアログ2

手法は三角ボタンの作成で使った オーナードローで処理します。つまり、チェックをした時/しない時、 フォーカス/非フォーカス時などすべての状況を考慮してプログラマ(あなた)が描くことになります。 オーナードローなんていかにもプログラミングやってるぞというような名前ですが、 実際は地味な描画作業が続いてイライラするかもしれませんが我慢してください。 作業概要は次のとおりです。

  1. チェックボックスはCButtonで扱うので派生クラスを 作成します。オーナードローを行うにはCButton::DrawItemをオーバーライドして、 ここにチェックボックスを状況(チェック/非チェックなど)に応じて描画していきます。 従ってここの実装が今回の中心です。
  2. オーナードローでチェックボックスを描画した場合、 CButton::GetCheck、CButton::SetCheckは無効になるようなので、あらためて派生クラスで定義します。
  3. CButton派生クラスができたら、CWnd::SubClassDlgを使って ダイアログクラス(CxxxDlg)のサブクラスにします。その際、フレームワークから上記のDrawItemが 呼ばれるようにするためにボタンのスタイルをBS_OWNERDRAWに設定します。
  4. 上記のダイアログ1・2のチェック反転ボタンに、 2.の関数を使ってオーナードローチェックボックスの チェック・非チェックの動作を実装します(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)の実装を上記のようにしてください。