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でこのボタンのときに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も)。
