MFC VC++ 自定义复选框图像

3
如何使3态复选框在Indeterminate状态下使用不同的位图?
我想改变我的3态复选框使用不同的图片;这些控件是Win98风格的,这种复选框的不确定状态很难与禁用的复选框区分开来(这可能是为什么他们为WinXP样式控件更改了这个,但由于项目中的其他细节,我不能使用那些)。我正在使用Visual C++ 2010,并且在VS的资源编辑器中定义了一个8x8位图。位图的ID是IDB_INDET_CHECK。
我不太确定像这样的标准“技术”是什么;我只是开始进入操作Windows控件和MFC。我的第一次尝试是创建一个类CTriButton,它从CButton派生,重写DrawItem函数,并尝试自己绘制它。然后我使用SubclassDlgItem将我的窗口中的一个复选框转换为这个类(我认为是这样?)。这...有点起作用?复选框不再出现,如果我点击应该出现的位置,一个空的复选框框架出现,但没有其他事情发生(我的代码中的调试消息也没有被发送)。
以下是相关代码,虽然我不确定任何这些是否正确。首先,是窗口的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;
}

接下来是我自定义按钮的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);
}

1
你在哪里启用了自绘窗口样式? - paludarium
1
你有什么问题?你在哪里设置复选框以拥有三态或将按钮状态设置为中间状态? - Ajay
@paludarium:谢谢,这有所帮助;我不知道该怎么做。我已经更新了我的代码并设置了ownerdraw样式;现在这个东西似乎根本没有被绘制出来,直到我点击它应该在的地方,然后我只得到一个空的复选框框架。我的绘图代码似乎没有被调用(从未看到调试消息)。我的问题已经更新了。感谢提供设置样式的建议! - KRyan
@Ajay:复选框是在Visual Studio的资源编辑器中设置的,包括其三态和默认状态。不管怎样,感谢你的评论;我已经尝试重新修改我的问题以使其更清晰! - KRyan
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 的调用可能没有产生预期的效果。此调用使旨在为按钮处理的消息首先由控件父窗口处理。因为CWnd(CDialog的超类)中DrawItem的默认实现不执行任何操作,因此消息永远不会传递到控件。
请将其替换为以下片段,一切都应该没问题:
HWND hWndButton;
GetDlgItem(IDC_HEAD, &hWndButton);
CBipedHead.SubclassWindow(hWndButton);

这里有两个小提示:

  1. 通常不建议在类和类成员中使用相同的命名规则,这会让阅读变得混乱。
  2. 我猜想你总是在编译和运行时处于发布模式。如果是这样,请不要这么做。这会防止断言被抛出并让你知道有什么问题。

CButton::DrawItem 什么也没做?那真是奇怪。有没有办法加载复选标记的默认位图(一些默认ID或可用于LoadBitmap的内容)?此外,DrawItem 函数需要处理哪些其他事项,即我的代码是否都正确了?对我来说,其中一个主要问题是我无法看到 DrawItem 的实际定义,以知道我应该模仿什么...任何帮助都将不胜感激。感谢您提供关于 InvalidateRectUpdateWindow 的说明;这很有意义。 - KRyan
1
@DragoonWraith在MFC中,常见的控件表示形式都没有在它们的DrawItem()方法中实现渲染。它们通常只包含一行代码,就是一个ASSERT()语句。DRAWITEMSTRUC结构体包含一个名为_itemState_的成员,用于保存有关如何绘制控件的各种标志。对于所有的控件,您应该至少处理ODS_DEFAULT、ODS_DISABLED、ODS_FOCUS和ODS_SELECTED状态。据我所知,常见的控件通常不依赖于资源位图来显示它们的元素。 - Captain Obvlious
1
@DragoonWraith 非常感谢。您可以使用位图或使用基于矢量的GDI调用进行绘制。需要记住的一件事是,您应该尽一切努力在对话框和窗口中显示的所有控件的外观和感觉上保持统一性。这可能需要额外的时间,但它为您的用户提供了更流畅的演示。 - Captain Obvlious
哦,我完全同意;这就是我做这个的全部原因(Win98风格的复选框具有非常不令人满意的不确定状态,因此我基本上正在重新创建它们的WinXP外观)。应用程序中的所有3态框都将使用相同的样式。由于大多数用户使用WinXP或更高版本,我认为这比另一种选择更加熟悉。 - KRyan
@Chad - 他正在使用按钮控件的自定义/所有者绘制功能。 - Captain Obvlious
显示剩余6条评论

1
不是唯一的答案,而是一个答案:我找到了这个自定义CCheckBox,它更或多或少能够实现我想要的功能。默认情况下它不允许三种状态,但我用一些自己的调整解决了这个问题。我不能100%确定它能够直接使用(我有一些问题,并不似乎是由于我的修改引起的,但我无法确定),但这是我使用的解决方案。我不会称之为唯一的答案,因为如果有人发现了我的代码错误并想要启示我,那就好了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接