MFC VC++ Custom Checkbox Image

KRyan picture KRyan · Jul 16, 2011 · Viewed 8.2k times · Source

How do I get a 3-state checkbox to use a different bitmap for the Indeterminate state?

I want to change the image used by my 3-state checkboxes to use a different one; the controls are in Win98-style, and the indeterminate state of such checkboxes is difficult to distinguish from disabled checkboxes (this is presumably why they changed this for the WinXP-style controls, but I cannot use those because of other details in my project).

I'm using Visual C++ 2010, and I've defined an 8x8 bitmap in VS's Resource Editor. The bitmap's ID is IDB_INDET_CHECK.

I'm not entirely sure what the standard "technique" for something like this is; I've only really just started getting into manipulating Windows controls and MFC.

My first attempt was to create a class, CTriButton, that derives from CButton, override the DrawItem function, and try to draw it myself. I then used SubclassDlgItem to turn one of the checkboxes in my window into this class (I think?). This... sort of works? The checkbox no longer appears, and if I click on where it should be, an empty checkbox frame appears, but nothing else happens (and the debug message in my code is not being sent).

Here's the relevant code, though I'm not sure any of this is right. First, code from my window's OnInitDialog.

BOOL CAffixFilterDlg::OnInitDialog() // CAffixFilterDlg is my CDialog-derived window
{
    CDialog::OnInitDialog(); // call basic version

    // subclass a CButton-derived control with CTriButton
    if ( CBipedHead.SubclassDlgItem(IDC_HEAD, this) ) // CBipedHead is a CTriButton member of CAffixFilterDlg, IDC_HEAD is a checkbox
        SetWindowLong(CBipedHead.m_hWnd, GWL_STYLE, CBipedHead.GetStyle() | BS_OWNERDRAW); // set the ownerdraw style
    else // subclassing didn't work
        _ERROR("Subclassing failed."); // I do not see this error message, so SubclassDlgItem worked?

    // initialization continues, but is not relevant...
    UpdateWindow();
    Invalidate();

    return TRUE;
}

Next, the code for my custom button's DrawItem.

void CTriButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    _DMESSAGE("Drawing TriButton"); // never see this message

    CDC dc;
    dc.Attach(lpDrawItemStruct->hDC);     //Get device context object
    int nWidth = GetSystemMetrics(SM_CXMENUCHECK);
    int nMargin = ( nWidth - 8 ) / 2;

    CRect textRt = lpDrawItemStruct->rcItem;
    textRt.right = textRt.right - nWidth - nMargin;

    CString text;
    GetWindowText(text);

    UINT textDrawState = DST_TEXT;
    if ( lpDrawItemStruct->itemState & ODS_DISABLED )
        textDrawState |= DSS_DISABLED;

    dc.DrawState(CPoint(textRt.left, textRt.top), textRt.Size(), text, textDrawState, TRUE, 0, (CBrush*)NULL);

    CRect rt = lpDrawItemStruct->rcItem;    // initial rect is for entire button
    rt.left = rt.right - nWidth;            // set left margin
    LONG center = ( rt.bottom + rt.top ) / 2;
    rt.top = center - nWidth/2;
    rt.bottom = center + nWidth/2;

    UINT checkDrawState = DFCS_BUTTONCHECK;
    if ( lpDrawItemStruct->itemState & ODS_DISABLED )
        checkDrawState |= DFCS_INACTIVE;

    if ( lpDrawItemStruct->itemState & ODS_CHECKED )
        checkDrawState |= DFCS_CHECKED;

    else if ( GetCheck() == BST_INDETERMINATE ) {
        _VMESSAGE("Indeterminate; custom draw.");

        CBitmap indet_check = CBitmap();
        indet_check.LoadBitmap(IDB_INDET_CHECK);

        CPoint pt = CPoint(rt.left + nMargin, rt.top + nMargin);
        CSize sz = CSize(8, 8);

        dc.DrawState(pt, sz, &indet_check, DST_BITMAP|DSS_NORMAL);
    }

    dc.DrawFrameControl(rt, DFC_BUTTON, checkDrawState);
}

Answer

Captain Obvlious picture Captain Obvlious · Jul 17, 2011

In OnInitDialog() you need to call InvalidateRect() after changing the window style otherwise it doesn't know it needs to be redrawn. It's also a good idea to call UpdateWindow() after changing window styles. Some information is usually cached by the common controls and won't acknowledge the change until UpdateWindow() has been called.

In DrawItem() you are responsible for rendering all states of the control. You should not call CButton::DrawItem() as it does nothing. Try something like the following:

void CTriButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    CBitmap indet_check

    _DMESSAGE("Drawing TriButton"); // I never see this message
    int checkState = GetCheck();

    if ( checkState == BST_CHECKED )
    {
        indet_check.LoadBitmap(IDB_INDET_CHECK);
    }
    else if ( checkState == BST_UNCHECKED )
    {
        indet_check.LoadBitmap(IDB_INDET_UNCHECKED);
    }
    else if ( checkState == BST_INDETERMINATE )
    {
        indet_check.LoadBitmap(IDB_INDET_INDETERMINATE);
    }

    //    ... rest of your drawing code here ...
    //    don't forget to draw focus and push states too ;)

}

Addendum:

I can't believe I missed this first time around but your call to SubclassDlgItem is probably not having the desired effect. This call causes messages intended for the button to be processed by the controls parent window first. Because the default implementation of DrawItem in CWnd (the superclass of CDialog) does nothing the message never gets passed to the control.

Replace this with the following snippet and everything should be ok:

HWND hWndButton;
GetDlgItem(IDC_HEAD, &hWndButton);
CBipedHead.SubclassWindow(hWndButton);

Two side notes here:

  1. It's usually not a good idea to use the same naming convention for both classes and class members. It makes for a confusing read.
  2. I'm guessing you are always compiling and running in release mode. If you are - don't. This prevents assertions from being thrown and letting you know something is wrong.