【MFC】ボタンの色がOnCtlColorで変わらない:BS_OWNERDRAWとDrawItemで背景色と文字色を変更する

【MFC】ボタンの色がOnCtlColorで変わらない:BS_OWNERDRAWとDrawItemで背景色と文字色を変更する

MFCのダイアログで、非常停止のようなボタンを赤くしようとしてOnCtlColorでCTLCOLOR_BTNのときに赤いブラシを返しても、プッシュボタンの面と文字の色は変わりません。プッシュボタンは、OnCtlColorが返したブラシを使わずにシステムの色で描かれるためです。ボタンの背景色と文字色を変更するには、ボタンをBS_OWNERDRAWにしてDrawItemで自分で描くか、CMFCButtonに替えてSetFaceColorを使います。


目次

OnCtlColorでプッシュボタンの色が変わらない理由

ボタンを描く前に、親のダイアログにはWM_CTLCOLORBTNが送られ、MFCではOnCtlColorがnCtlColorにCTLCOLOR_BTNを入れて呼ばれます。ただし、BS_PUSHBUTTON・BS_DEFPUSHBUTTON・BS_PUSHLIKEのボタンは返したブラシを使わず、いつもシステムの既定の色で描かれます(WM_CTLCOLORBTN)。プッシュボタンを立体に見せるには面・明るい縁・影の縁で何本かのブラシが要りますが、このメッセージで返せるブラシは1本だけだからです。SetTextColorで決めた文字色も、プッシュボタンではフォーカスの枠にしか効かず、ボタンの文字の色は変わりません。

OnCtlColorで赤いブラシを返したプッシュボタンは既定の色のままで、BS_OWNERDRAWとDrawItemで描いたボタンとCMFCButtonのSetFaceColorを使ったボタンは赤や緑になっている画面
OnCtlColorで赤を返した上段は既定の色のまま。DrawItemで描いた中段とCMFCButtonの下段は色が変わる

上段は、OnCtlColorでこのボタンのときにSetTextColorで文字色を白にし、赤いブラシを返したプッシュボタンです。OnCtlColorは再描画のたびに呼ばれますが、面は既定の色、文字は黒のままで、外周の細い部分だけが赤くなります。中段はBS_OWNERDRAWにしてDrawItemで描いたボタン、下段はCMFCButtonのSetFaceColorで赤にしたボタンです。エディットやスタティックの色をOnCtlColorで変える方法はOnCtlColorでコントロールの背景色を変えるにあります。

方法面の色文字の色書くこと
OnCtlColorでCTLCOLOR_BTNのときにブラシを返す変わらない変わらないなし(効かない)
BS_OWNERDRAWにしてDrawItemで描く変わる変わるCButtonの派生クラスと、押したとき・無効のとき・フォーカスの描き分け
CMFCButtonのSetFaceColorとSetTextColor変わる変わる変数の型をCMFCButtonにして、色を2行で指定する

BS_OWNERDRAWとDrawItemでボタンの背景色と文字色を変更する

オーナードローにすると、ボタンの見た目は全部アプリが描きます。色は自由に決められる代わりに、押したときに沈んで見せることや、無効のときに灰色にすることも自分で書きます。

手順1: ボタンにBS_OWNERDRAWを付ける

.rcのボタンの行にBS_OWNERDRAWを足します。BS_OWNERDRAWは、ほかのボタンのスタイルと組み合わせません(ボタンのスタイル)。

PUSHBUTTON      "非常停止",IDC_BUTTON_STOP,92,74,64,20,BS_OWNERDRAW
PUSHBUTTON      "リセット",IDC_BUTTON_RESET,166,74,64,20,BS_OWNERDRAW | WS_DISABLED

PUSHBUTTONはBS_PUSHBUTTON(値は0)のボタンなので、BS_OWNERDRAWを足した結果はBS_OWNERDRAWだけのボタンになります。

手順2: CButtonの派生クラスでDrawItemを書く

オーナードローのボタンは、描くときに親のダイアログへWM_DRAWITEMを送ります。MFCではCWnd::OnDrawItemがこのメッセージをボタン自身に戻し(リフレクション)、CButton::DrawItemが呼ばれます。CButton::DrawItemの中身はMFCのソース(winctrl1.cpp)ではASSERT(FALSE)だけで、派生クラスで書き直す前提になっています。そこで、色を持たせたCButtonの派生クラスを作ります。

class CColorButton : public CButton
{
// Construction
public:
    CColorButton();

// Operations
public:
    void SetColors(COLORREF crFace, COLORREF crText);

// Overrides
public:
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);

// Implementation
protected:
    COLORREF m_crFace;
    COLORREF m_crText;

    DECLARE_MESSAGE_MAP()
};

DrawItemでは、DRAWITEMSTRUCTのhDCに、rcItemの範囲を描きます。押したとき・無効のとき・フォーカスがあるときは、itemStateにそれぞれODS_SELECTED・ODS_DISABLED・ODS_FOCUSが立っています(DRAWITEMSTRUCT)。

static COLORREF DarkenColor(COLORREF cr)
{
    return RGB(GetRValue(cr) * 3 / 4, GetGValue(cr) * 3 / 4, GetBValue(cr) * 3 / 4);
}

BEGIN_MESSAGE_MAP(CColorButton, CButton)
END_MESSAGE_MAP()

CColorButton::CColorButton()
    : m_crFace(::GetSysColor(COLOR_BTNFACE))
    , m_crText(::GetSysColor(COLOR_BTNTEXT))
{
}

void CColorButton::SetColors(COLORREF crFace, COLORREF crText)
{
    m_crFace = crFace;
    m_crText = crText;

    if (GetSafeHwnd() != NULL)
        Invalidate();
}

void CColorButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);

    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    ASSERT_VALID(pDC);

    CRect rect(lpDrawItemStruct->rcItem);
    const UINT nState = lpDrawItemStruct->itemState;
    const BOOL bPressed = (nState & ODS_SELECTED) != 0;
    const BOOL bDisabled = (nState & ODS_DISABLED) != 0;

    const int nSavedDC = pDC->SaveDC();

    COLORREF crFace = m_crFace;
    COLORREF crText = m_crText;
    if (bDisabled)
    {
        crFace = ::GetSysColor(COLOR_BTNFACE);
        crText = ::GetSysColor(COLOR_GRAYTEXT);
    }
    else if (bPressed)
    {
        crFace = DarkenColor(m_crFace);
    }

    pDC->FillSolidRect(rect, crFace);
    pDC->Draw3dRect(rect, DarkenColor(crFace), DarkenColor(crFace));

    CString sText;
    GetWindowText(sText);

    CRect rectText(rect);
    if (bPressed)
        rectText.OffsetRect(1, 1);

    pDC->SetBkMode(TRANSPARENT);
    pDC->SetTextColor(crText);
    pDC->DrawText(sText, rectText, DT_SINGLELINE | DT_CENTER | DT_VCENTER);

    if ((nState & ODS_FOCUS) != 0 && !bDisabled)
    {
        CRect rectFocus(rect);
        rectFocus.DeflateRect(3, 3);
        pDC->SetTextColor(RGB(0, 0, 0));
        pDC->SetBkColor(RGB(255, 255, 255));
        pDC->DrawFocusRect(rectFocus);
    }

    pDC->RestoreDC(nSavedDC);
}

押したときは面を4分の3の明るさにし、文字を右下へ1ピクセルずらして沈んだように見せています。無効のときは、色の指定に関係なく面をシステムのボタンの色、文字を灰色にしています。上の画面の中段右のリセットが、無効のときの描画です。渡されたDCに選択したGDIオブジェクトは、DrawItemを抜ける前に戻しておく決まりなので(CButton::DrawItem)、SaveDCとRestoreDCで描く前の状態に戻しています。

手順3: ダイアログの変数をCColorButtonにして色を決める

ダイアログのメンバー変数の型をCColorButtonにし、DDX_Controlでボタンと結び付けます。変数の型をCButtonのままにすると、CButton::DrawItemのASSERT(FALSE)に進みます。色はOnInitDialogで決めます。

// CPanelDlg.h
CColorButton m_btnStop;
CColorButton m_btnReset;

// CPanelDlg.cpp
void CPanelDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_BUTTON_STOP, m_btnStop);
    DDX_Control(pDX, IDC_BUTTON_RESET, m_btnReset);
}

BOOL CPanelDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_btnStop.SetColors(RGB(200, 30, 30), RGB(255, 255, 255));
    m_btnReset.SetColors(RGB(200, 30, 30), RGB(255, 255, 255));

    return TRUE;
}

運転中だけ緑にするように、あとから色を変えたいときは、SetColorsを呼び直します。SetColorsの中でInvalidateを呼んでいるので、次の描画で新しい色になります。


CMFCButtonのSetFaceColorでボタンの色を変える

描き分けまで自分で書かずに済ませたいときは、CMFCButtonを使う方法もあります。CMFCButtonはCButtonの派生クラスで、SetFaceColorで面の色、SetTextColorで文字の色を指定できます。ヘッダーはafxbutton.hです。

// CPanelDlg.h
CMFCButton m_btnStop;

// CPanelDlg.cpp(DoDataExchange)
DDX_Control(pDX, IDC_BUTTON_STOP, m_btnStop);

// CPanelDlg.cpp(OnInitDialog)
m_btnStop.SetFaceColor(RGB(200, 30, 30));
m_btnStop.SetTextColor(RGB(255, 255, 255));

.rcのボタンはBS_OWNERDRAWを付けない普通のPUSHBUTTONのままです。ビジュアルマネージャーを設定していないダイアログアプリでは、上の画面の下段のように赤い面に白い文字になります。CMFCButtonは自分で描いているので、縁の形はWindowsの普通のボタンとは違います。

ウィザードで作ったダイアログアプリは、InitInstanceでCMFCVisualManagerWindowsをビジュアルマネージャーに設定しています。この場合、CMFCButtonはSetFaceColorの色で面を塗ったあと、その上にテーマのボタンを描くので、面は既定の色に戻り、白にした文字はほとんど見えなくなります。そのときは、SetFaceColorの前にm_btnStop.m_bDontUseWinXPTheme = TRUE;を書いて、テーマを使わずに描かせます。


まとめ

  • プッシュボタンの面と文字の色は、OnCtlColorで返したブラシやSetTextColorでは変わらない。
  • BS_OWNERDRAWを付けて、CButtonの派生クラスのDrawItemでitemStateを見ながら描く。
  • 描き分けを書かないなら、CMFCButtonのSetFaceColorとSetTextColorを使う(ビジュアルマネージャーがあるときはm_bDontUseWinXPThemeも)。
目次