3

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);
}
4

2 回答 2

3

在 OnInitDialog() 中,您需要在更改窗口样式后调用 InvalidateRect() ,否则它不知道需要重绘。在更改窗口样式后调用 UpdateWindow() 也是一个好主意。某些信息通常由公共控件缓存,并且在调用 UpdateWindow() 之前不会确认更改。

在 DrawItem() 中,您负责呈现控件的所有状态。你不应该调用 CButton::DrawItem() 因为它什么都不做。尝试以下操作:

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 ;)

}

附录:

我不敢相信我第一次错过了这个,但你的电话SubclassDlgItem可能没有达到预期的效果。此调用导致用于按钮的消息首先由控件父窗口处理。因为DrawItemin CWnd(CDialog 的超类)的默认实现什么都不做,所以消息永远不会传递给控件。

将其替换为以下代码段,一切都应该没问题:

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

这里有两个旁注:

  1. 对类和类成员使用相同的命名约定通常不是一个好主意。它使阅读变得混乱。
  2. 我猜你总是在发布模式下编译和运行。如果你是 - 不要。这可以防止断言被抛出并让您知道有问题。
于 2011-07-17T17:21:32.950 回答
1

不是答案,而是答案:我发现这种习惯CCheckBox或多或少地实现了我想要的。默认情况下,它不允许 3 种状态,但我通过自己的一些调整来解决这个问题。我不是 100% 确定它开箱即用(我遇到了一些问题,这似乎不是由于我的编辑,但我不能确定),但这是我的解决方案用过的。不过,我不会将此称为答案,以防有人可以窥探我的代码出了什么问题并想照亮我。

于 2011-07-24T04:55:05.290 回答