支持三态CheckBox的 MFC TreeCtrl 控件扩展

本文介绍如何在MFC中实现一个支持三态CheckBox的TreeCtrl控件,包括自绘CheckBox图片、垂直居中对齐及事件处理等关键步骤。

前言: 如果选择 MFC 来做界面, 那么, MFC 中各种功能有限的控件一定让你蛋疼不已. 比如, TreeCtrl  的 CheckBox 居然不支持三态以及 CheckBox 居然不能垂直居中对齐. 下面我将为大家介绍如何实现一个支持三态 CheckBox 的 TreeCtrl 控件, 谨作抛砖引玉.

首先, 从 CtreeCtrl 派生一个类, 姑且命名为 CExTreeCtrl.  

第二,响应 NM_CUSTOMDRAW 消息来自绘 TreeItem  (仅仅要支持三态的话不用重绘, 但是要垂直居中的话就免不了了). 另外再上今天的主角, 虚函数 DrawItem(HTREEITEM hItem, LPNMTVCUSTOMDRAW lpNMTVCD), 由它负责绘制 Item. 以下我将详细说明 OnNMCustomdraw 与 DrawItem 的实现:

void CExTreeCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR);
    LPNMTVCUSTOMDRAW lpNMTVCD = (LPNMTVCUSTOMDRAW) pNMCD;
    //默认处理
    *pResult = CDRF_DODEFAULT;
    // TODO: Add your control notification handler code here
    if(lpNMTVCD->nmcd.dwDrawStage == CDDS_PREPAINT)
    {//返回 CDRF_NOTIFYITEMDRAW 标志, 告诉系统通知你每个绘制的细节
        *pResult = CDRF_NOTIFYITEMDRAW;
        return;
    }
    else if(lpNMTVCD->nmcd.dwDrawStage == CDDS_ITEMPREPAINT) 
   {//在 ItemPrePaint 状态实现自绘 
        CRect rcItem(pNMCD->rc); 
        CPoint ptItem(rcItem.left + 1, rcItem.top + 1); 
        UINT uFlags = 0; 
        HTREEITEM hItem = HitTest(ptItem, &uFlags);
        //取出要绘制的 Item 的句柄 
        if(!hItem) return; DrawItem(hItem, lpNMTVCD); 
        //返回 CDRF_SKIPDEFAULT 标志, 告诉系统跳过默认处理
        *pResult = CDRF_SKIPDEFAULT; return;
    }
}


void CExTreeCtrl::DrawItem( HTREEITEM hItem, LPNMTVCUSTOMDRAW lpNMTVCD )
{
    DWORD dwStyle = GetStyle();
    CRect rcItem;
    CRect rcTemp;
    CPoint ptTemp;
    UINT indent = GetIndent();
    //取得 Item 文本的矩形范围
    GetItemRect(hItem, &rcItem, TRUE);
    CDC dc;
    dc.Attach(lpNMTVCD->nmcd.hdc);

    if(lpNMTVCD->nmcd.uItemState & CDIS_SELECTED)
    {//如果 Item 被选中, 填充背景 (背景色我乱写的)
        dc.FillSolidRect(&rcItem, RGB(255, 123, 128));
    }

    dc.SetBkMode(TRANSPARENT);
    dc.SetTextColor(RGB(0, 0, 0));

    rcTemp = rcItem;
    rcTemp.left += 2;
    rcTemp.top += 2;
    //绘制文本
    dc.DrawText(GetItemText(hItem), &rcTemp, DT_LEFT | DT_SINGLELINE | DT_VCENTER);

    int nImage, nImageSelected;

    //取出与 Item 关联的 ImageList
    CImageList* imageList = GetImageList(TVSIL_NORMAL);
    if(imageList && GetItemImage(hItem, nImage, nImageSelected))
    {//绘制与 Item 相关联的 image
        rcItem.left -= 18;//据我目测,系统本身是在 18px 宽的矩形内绘制图片的
        rcItem.right = rcItem.left + 18;
        CPoint pt(rcItem.left, rcItem.top);
        IMAGEINFO imageInfo = {0};
        imageList->GetImageInfo( ((lpNMTVCD->nmcd.uItemState & CDIS_SELECTED ) ? nImageSelected : nImage), &imageInfo);
        //center alignment
        pt.y += ( rcItem.Height() - imageInfo.rcImage.bottom + imageInfo.rcImage.top ) / 2;
        //pt.x += ( rcItem.Width() - imageInfo.rcImage.right + imageInfo.rcImage.left ) / 2;
        imageList->Draw(&dc, nImage, pt, ILD_NORMAL);
        }

    //取出 CheckBox 的各个选中状态的图片,系统本身的CheckBox只支持两态, 因此得自己准备一张包含各个状态图片的位图资源, 并通过SetImageList(XX, TVSIL_STATE)将它设置到 ExTreeCtrl中, 图片规格(以索引说明): 0-空白图片,1-未选中状态图片, 2-选中状态图片, 3-部分选中状态图片
    imageList = GetImageList(TVSIL_STATE);
    if(imageList && (dwStyle & TVS_CHECKBOXES))
    {//绘制CheckBox相应状态的图片
        //将状态码变换成图片在ImageList中的索引, MFC中有一个宏是把索引变换到StateImageState的, 实际就是 <<12 操作, 因此反过来也成立
        nImage = GetItemState( hItem, TVIS_STATEIMAGEMASK ) >> 12;
        rcItem.left -= 18;//据我目测..., 
        rcItem.right = rcItem.left + 18;
        CPoint pt(rcItem.left, rcItem.top);
        IMAGEINFO imageInfo = {0};
        imageList->GetImageInfo(nImage, &imageInfo);
        //居中对齐
        pt.y += ( rcItem.Height() - imageInfo.rcImage.bottom + imageInfo.rcImage.top ) / 2;
        pt.x += ( rcItem.Width() - imageInfo.rcImage.right + imageInfo.rcImage.left ) / 2;
        imageList->Draw(&dc, nImage, pt, ILD_TRANSPARENT);
    }
    //如果要画线
    if(dwStyle & TVS_HASLINES)
    {
        //创建一个真正的点线画笔
        LOGBRUSH logBrush;
        logBrush.lbColor = RGB(0xa2, 0xa2, 0xa2);
        logBrush.lbStyle = BS_SOLID;
        CPen pen(PS_COSMETIC | PS_ALTERNATE, 1, &logBrush);
        CPen* oldPen = dc.SelectObject(&pen);

        rcTemp = rcItem;
        rcTemp.left -= indent;//从当前向左移动一个 缩进 的量
        rcTemp.right = rcTemp.left + indent;
        ptTemp = rcTemp.CenterPoint();

        //如果 Item 有父 Item 则在自己面前画一个'L'型的线,拐点正是缩进矩形的中心点
        if( GetParentItem(hItem) != NULL )
        {
            //如果 Item 有一个弟弟节点(哥哥排上面), 则在自己面前画一条竖线, 否则画半条
            dc.MoveTo( ptTemp.x - 1,rcTemp.top - 1 );
            if(GetNextSiblingItem(hItem) != NULL)
                dc.LineTo( ptTemp.x - 1, rcTemp.bottom );
            else dc.LineTo( ptTemp.x - 1, pt.y - 1 );
            dc.MoveTo( rcTemp.right, ptTemp.y - 1 );
            dc.LineTo( ptTemp.x - 1, ptTemp.y - 1 );
        }
        //依次绘制各个 Item 的父节点与叔叔节点之间被撑开的部分的连线
        HTREEITEM hItemTemp = hItem;
        while( hItemTemp = GetParentItem(hItemTemp) )
        {
            rcTemp.OffsetRect(-indent, 0);
            ptTemp = rcTemp.CenterPoint();
            if(GetNextSiblingItem(hItemTemp))
            {
                dc.MoveTo( ptTemp.x - 1, rcTemp.top );
                dc.LineTo( ptTemp.x - 1, rcTemp.bottom );
            }
        }
        //显示删除 MFC GDI 对象, 因为以前版本的 MFC 貌似有个BUG, 如果你不显示删除它就不会帮你删除, 这个BUG好像还存在
        dc.SelectObject(oldPen);
        pen.DeleteObject();
        pen.DeleteTempMap();
    }
    //绘制小'+'框
    if(dwStyle & TVS_HASBUTTONS)
    {
        rcItem.left -= indent;
        rcItem.right = rcItem.left + indent;
        //确定绘制范围
        rcTemp.SetRect(0, 0, 9, 9);
        rcTemp.OffsetRect(rcItem.CenterPoint());
        rcTemp.OffsetRect(-5, -5);
        //绘制, 因为使用的是MFC10.0, 因此实际绘制工作可交给 CMFCVisualManager 完成,其实... 自己画难度也不大
        if( ItemHasChildren(hItem) ) CMFCVisualManager::GetInstance()->OnDrawExpandingBox(&dc, rcTemp, ( GetItemState(hItem, TVIS_EXPANDED) & TVIS_EXPANDED ?TRUE:FALSE), RGB(0, 0, 0));
    }
    //完成
    dc.Detach();
}

接下来处理事件, 让支持三态 CheckBox 的TreeCtrl 能够根据自己的状态改变父节点与子节点的状态, 因此, 再添加几个辅助函数来帮助我们完成. 

1, TVM_SETITEM 的响应函数, 让它处理State变化的情况. 当然, 重载SetItemState也是可以的, 但不完美, 因为有些程序员就是喜欢用 SendMessage(TVM_SETITEM, XX,XXX) 来破坏你精心设计好的重载. 因此, 响应该消息就能解决问题. 以下是函数的实现:

LRESULT CExTreeCtrl::DoSetItem( WPARAM wParam, LPARAM lParam )
{

    LPTVITEM lpItem = (LPTVITEM)lParam;
    UINT uItemState = 0;
    //如果是设置 State 就自定义处理
    if(lpItem && (lpItem->mask & TVIF_STATE)) 
    { 
        uItemState = lpItem->state >> 12; 
        //设置父节点与子节点的状态 
        SetParentState(lpItem->hItem, uItemState);
        SetChildrenState(lpItem->hItem, uItemState); 
    } 
    //调用默认处理 
    return DefWindowProc(TVM_SETITEM, wParam, lParam);
}

2, SetParentState 函数, 用来设置父节点状态, 如果本层所有节点状态一致, 则父节点与本层状态一致, 否则父节点应该是部分选中的状态. 以下具体实现:

void CExTreeCtrl::SetParentState( HTREEITEM hCurrentItem, UINT nState )
{
    if(hCurrentItem == NULL) return;
    //如果状态未改变, 则直接返回
    if( (GetItemState(hCurrentItem, TVIS_STATEIMAGEMASK) >> 12) == nState ) return;

    HTREEITEM hParentItem = GetParentItem(hCurrentItem);
    if(hParentItem == NULL) return;
    //从当前节点开始循环遍历, 直到遇到与当前节点状态不一致的节点
    for(HTREEITEM hItemItr = GetNextSiblingItem(hCurrentItem); hItemItr != hCurrentItem; )
    {

        if( (hItemItr != NULL) && (nState != GetItemState(hItemItr, TVIS_STATEIMAGEMASK) >> 12) )
        {//不一致, 则父节点应该为部分选中状态
            nState = 3;
            break;
        }
        hItemItr = GetNextSiblingItem(hItemItr);
        if(hItemItr == NULL) hItemItr = GetChildItem(hParentItem);
    }

    //如果父节点状态与要设置的状态相同, 则返回
    if( (GetItemState(hParentItem, TVIS_STATEIMAGEMASK) >> 12) == nState ) return;
    //递归
    SetParentState(hParentItem, nState);

    TVITEM item;
    item.hItem = hParentItem;
    item.mask = TVIF_STATE;
    item.pszText = (LPTSTR) 0;
    item.iImage = 0;
    item.iSelectedImage = 0;
    item.state = INDEXTOSTATEIMAGEMASK(nState);
    item.stateMask = TVIS_STATEIMAGEMASK;
    item.lParam = 0;
    //调用默认处理过程
    DefWindowProc(TVM_SETITEM, 0, (LPARAM)&item);
}

3, SetChildrenState, 设置子节点的状态. 子节点的状态应该与自身状态一致.

void CExTreeCtrl::SetChildrenState( HTREEITEM hCurrentItem, UINT nState )
{
    if(hCurrentItem == NULL) return;
    //自身状态未改变, 则退出
    if( (GetItemState(hCurrentItem, TVIS_STATEIMAGEMASK) >> 12) == nState ) return;
    //遍历所有子节点, 并处理
    TVITEM item;
    for(HTREEITEM hItemItr = GetChildItem(hCurrentItem); hItemItr != NULL; hItemItr = GetNextSiblingItem(hItemItr))
    {
        SetChildrenState(hItemItr, nState);

        item.hItem = hItemItr;
        item.mask = TVIF_STATE;
        item.pszText = (LPTSTR) 0;
        item.iImage = 0;
        item.iSelectedImage = 0;
        item.state = INDEXTOSTATEIMAGEMASK(nState);
        item.stateMask = TVIS_STATEIMAGEMASK;
        item.lParam = 0;
        //调用默认处理过程
        DefWindowProc(TVM_SETITEM, 0, (LPARAM)&item);
    }
}

最后处理鼠标事件, 因为部分选中状态并不一个用户直接可以用鼠标操作就能设置的状态, 因此要屏蔽掉. 先在CExTreeCtrl中添加一个成员变量 CPoint m_ptClickPoint, 然后响应三个消息: WM_LBUTTONDOWN, WM_LBUTTONDBCLK 和 NM_CLICK, 下以是这三个函数的具体实现:

void CExTreeCtrl::OnNMClick(NMHDR *pNMHDR, LRESULT *pResult)
{
    // TODO: Add your control notification handler code here
    UINT uFlags = 0;
    //取得单击时的Item
    HTREEITEM hItem = HitTest(m_ptClickPoint, &uFlags);
    if(uFlags & TVHT_ONITEMSTATEICON)
    {
        //1: 未选中, 2: 选中, 3: 部分选中
        UINT uItemState = CTreeCtrl::GetItemState(hItem, TVIS_STATEIMAGEMASK) >> 12;
        uItemState = uItemState >=2 ? 1 : 2;
        CTreeCtrl::SetItemState(hItem, INDEXTOSTATEIMAGEMASK(uItemState), TVIS_STATEIMAGEMASK);
    }

    *pResult = 0;
}
void CExTreeCtrl::OnLButtonDblClk(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    UINT uHTFlags = 0;
    HitTest(point, &uHTFlags);
    //如果点击到了CheckBox, 则阻止原来的折叠动作
    if(uHTFlags & TVHT_ONITEMSTATEICON)
    {
        SendMessage(WM_LBUTTONDOWN, uHTFlags, MAKELONG(point.x, point.y));
        return;
    }

    CTreeCtrl::OnLButtonDblClk(nFlags, point);
}

void CLayertree::OnLButtonDown( UINT nFlags, CPoint point )
{
    //记录点击的位置
    m_ptClickPoint = point;
    CTreeCtrl::OnLButtonDown(nFlags, point);
}

好, 至此, 一个基本可用的支持三态CheckBox的TreeCtrl已经完成, 当然其中还有一些细节问题需要处理, 使用方式与原来的控件没有区别, 以下是范例:

CRect rectDummy;
rectDummy.SetRectEmpty();

// Create view:
const DWORD dwViewStyle = WS_CHILD | WS_VISIBLE | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_CHECKBOXES | TVS_SHOWSELALWAYS | TVS_HASLINES | 0;

if (!m_wndTree.Create(dwViewStyle, rectDummy, this, 4))
{
TRACE0("Failed to create file view\n");
return -1;      // fail to create
}

// Load view images:
m_LayertreeImges.Create(IDB_LAYRETREE, 16, 0, RGB(255, 0, 255));
m_CheckStateImages.Create(IDB_CHECK_STATE, 16, 0, RGB(255, 0, 255));
m_wndTree.SetImageList(&m_LayertreeImges, TVSIL_NORMAL);
m_wndTree.SetImageList(&m_CheckStateImages, TVSIL_STATE);
m_wndLayertree.SetItemHeight(24);

最后, 再说一句, C++本来就不擅长界面, 而随着PC硬件水平的提高, 采用C++来做界面所带来的性能优势会越来越不明显..

<think>我们在MFC控件(CTreeCtrl)中实现二态复选框(Tri-state Checkbox)。二态复选框通常有三种状态:选中(checked)、未选中(unchecked)和部分选中(indeterminate)。这在树控件中特别有用,例如当父节点的子节点只有部分被选中时,父节点可以显示为部分选中状态。 实现步骤: 1. 启用树控件的复选框功能:在创建树控件时,需要设置`TVS_CHECKBOXES`样式(或者通过`ModifyStyle`添加)。但是注意,默认的复选框样式只支持两种状态(选中和未选中)。为了实现三态,我们需要自定义复选框的图片。 2. 自定义复选框图片:我们可以使用图像列表(CImageList)来为树控件提供自定义的复选框图片。通常需要三张图片:未选中、选中和部分选中。 3. 处理树控件的通知消息:我们需要处理`TVN_ITEMCHANGING`或`TVN_ITEMCHANGED`消息,以及自定义绘制消息(如果需要更复杂的控制)来管理复选框的状态变化。 4. 状态更新逻辑:当用户点击一个节点的复选框时,需要更新该节点及其所有子节点的状态。同时,需要递归地更新父节点的状态(如果子节点状态不一致,则父节点设置为部分选中)。 5. 设置节点状态:使用`CTreeCtrl::SetItemState`来设置节点的状态,其中状态索引对应于图像列表中的图片索引。 具体实现: 步骤1:创建图像列表并设置树控件 - 创建一个包含三个状态的图像列表(16x16像素):索引0-未选中,1-选中,2-部分选中。 - 使用`CTreeCtrl::SetImageList`将图像列表关联到树控件,并设置`TVS_CHECKBOXES`样式(注意:设置自定义图像列表后,可能需要禁用`TVS_CHECKBOXES`样式,因为我们要用自己的图像)。 步骤2:插入树节点时,为每个节点设置初始状态图像。 - 使用`CTreeCtrl::InsertItem`插入节点,并通过`nStateImage`参数指定初始状态图像索引(1-based,因为0表示没有状态图像)。所以,我们可以将图像列表中的第一个图像(索引0)对应状态索引1,第二个对应2,第三个对应3。 步骤3:处理复选框点击事件。 - 我们可以通过处理`NM_CLICK`消息或者`TVN_KEYDOWN`(处理空格键)来捕获复选框的点击。 - 但是更常见的是处理`TVN_ITEMCHANGED`消息,但是注意,复选框状态变化也会触发该消息。然而,由于我们使用自定义状态图像,我们需要自己管理状态。 另一种方法是使用自定义绘制,但这里我们采用处理`NM_CLICK`消息的方法: - 在`OnClick`处理函数中,我们可以获取点击位置对应的项和点击的哪个部分(通过`HitTest`)。 - 如果点击在状态图像上,则切换当前节点的状态,并更新其子节点和父节点。 步骤4:更新节点状态 - 更新当前节点的状态(如果是未选中则变为选中,选中变为未选中?注意:我们有三态,所以通常的循环是:未选中->选中->部分选中?但是通常二态复选框只循环两种状态,而三态复选框通常由点击行为决定,我们这里需要根据需求设计状态切换逻辑。常见的做法是:点击节点时,如果当前是未选中,则设置为选中;如果当前是选中或部分选中,则设置为未选中。但是这样就没有利用三态。另一种做法是:点击只切换选中和未选中,而部分选中状态只能由子节点状态推导出来,不能直接设置。所以,我们通常只允许用户将节点设置为选中或未选中,而部分选中状态是自动计算的。 因此,我们这样设计: - 当用户点击一个节点时,我们强制将该节点设置为选中或未选中(如果当前是部分选中,则点击后设置为选中?或者根据当前状态切换?这里我们可以设定:如果当前是部分选中,则点击后设置为选中;如果是选中,则设置为未选中;如果是未选中,则设置为选中)。 - 然后,将该节点的所有子节点设置为与当前节点相同的状态(选中或未选中)。 - 最后,更新所有祖先节点的状态:祖先节点的状态由其子节点状态决定(如果所有子节点都是选中,则祖先节点为选中;所有子节点都是未选中,则祖先节点为未选中;否则为部分选中)。 步骤5:递归更新子节点 - 编写一个函数,递归设置某个节点下所有子节点的状态。 步骤6:递归更新父节点 - 编写一个函数,从当前节点的父节点开始,一直更新到根节点,更新每个节点的状态(根据其子节点的状态计算)。 示例代码框架: 1. 在对话框类中声明: CImageList m_checkStateImgList; 2. 初始化树控件和图像列表: // 创建图像列表 m_checkStateImgList.Create(16, 16, ILC_COLOR32 | ILC_MASK, 3, 1); // 添加三个状态图片:未选中、选中、部分选中(这里用颜色块代替,实际用位图) // 假设我们有一个函数添加图标,或者用CBitmap加载位图 CBitmap bmp; bmp.LoadBitmap(IDB_CHECKBOXES); // 这个位图包含三个状态,水平排列,每个16x16 m_checkStateImgList.Add(&bmp, RGB(255,0,0)); // 假设用红色作为掩码色 // 关联图像列表到树控件 m_treeCtrl.SetImageList(&m_checkStateImgList, TVSIL_STATE); m_checkStateImgList.Detach(); // 避免被树控件销毁,或者不分离,在对话框销毁时销毁图像列表 // 设置树控件样式,去掉TVS_CHECKBOXES,因为我们用自定义状态图 DWORD dwStyle = GetWindowLong(m_treeCtrl.m_hWnd, GWL_STYLE); dwStyle &= ~TVS_CHECKBOXES; SetWindowLong(m_treeCtrl.m_hWnd, GWL_STYLE, dwStyle | TVS_TRACKSELECT); // 插入节点时设置状态图像 HTREEITEM hParent = m_treeCtrl.InsertItem(_T("Parent"), 0, 0, TVI_ROOT); m_treeCtrl.SetItemState(hParent, INDEXTOSTATEIMAGEMASK(1), TVIS_STATEIMAGEMASK); // 初始未选中,状态图像索引1(对应图像列表中第一个图像) 注意:SetItemState的第二个参数是状态图像索引,使用INDEXTOSTATEIMAGEMASK宏,该宏将1-15的索引转换为0xF000作为高字,低字为0。而状态图像在图像列表中的索引是0开始,所以我们在设置状态图像索引时,通常用1表示图像列表中的第一个图像(索引0),2表示第二个(索引1),3表示第三个(索引2)。 3. 处理点击事件: void CMyDialog::OnClickTree(NMHDR* pNMHDR, LRESULT* pResult) { CPoint point; GetCursorPos(&point); m_treeCtrl.ScreenToClient(&point); UINT flags = 0; HTREEITEM hItem = m_treeCtrl.HitTest(point, &flags); if (hItem != NULL && (flags & TVHT_ONITEMSTATEICON)) { // 点击在状态图标上 int nImage = GetStateImage(hItem); // 获取当前状态图像索引(1,2,3) int nNewImage; if (nImage == 1) // 当前未选中 nNewImage = 2; // 设置为选中 else if (nImage == 2) // 当前选中 nNewImage = 1; // 设置为未选中 else // 部分选中,我们将其设置为选中 nNewImage = 2; // 设置当前节点的状态 m_treeCtrl.SetItemState(hItem, INDEXTOSTATEIMAGEMASK(nNewImage), TVIS_STATEIMAGEMASK); // 更新所有子节点 UpdateChildrenState(hItem, nNewImage); // 更新父节点状态 UpdateParentState(hItem); } *pResult = 0; } int CMyDialog::GetStateImage(HTREEITEM hItem) { UINT state = m_treeCtrl.GetItemState(hItem, TVIS_STATEIMAGEMASK); return (state >> 12) - 1; // 将状态图像掩码转换为1-15的索引,然后减1得到0-14,但我们的状态图像索引是1,2,3,所以这里这样转换:state>>12得到1-15的索引,然后就是1,2,3 // 或者直接:state>>12 就是我们设置时用的索引(1,2,3) } void CMyDialog::UpdateChildrenState(HTREEITEM hItem, int nNewImage) { // 递归设置所有子节点 HTREEITEM hChild = m_treeCtrl.GetChildItem(hItem); while (hChild != NULL) { m_treeCtrl.SetItemState(hChild, INDEXTOSTATEIMAGEMASK(nNewImage), TVIS_STATEIMAGEMASK); // 递归更新子节点的子节点 UpdateChildrenState(hChild, nNewImage); hChild = m_treeCtrl.GetNextSiblingItem(hChild); } } void CMyDialog::UpdateParentState(HTREEITEM hItem) { HTREEITEM hParent = m_treeCtrl.GetParentItem(hItem); if (hParent == NULL) return; // 检查所有兄弟节点(包括自己)的状态 HTREEITEM hChild = m_treeCtrl.GetChildItem(hParent); int nCheckedCount = 0; // 选中状态计数(状态2) int nUncheckedCount = 0; // 未选中计数(状态1) int nChildCount = 0; while (hChild != NULL) { int nState = GetStateImage(hChild); if (nState == 2) // 选中 nCheckedCount++; else if (nState == 1) // 未选中 nUncheckedCount++; // 注意:部分选中(3)的情况,这里我们暂时不考虑,因为子节点不会直接设置为部分选中(除非手动设置,但我们的更新逻辑不会) nChildCount++; hChild = m_treeCtrl.GetNextSiblingItem(hChild); } int nParentState; if (nCheckedCount == nChildCount) nParentState = 2; // 全部选中 else if (nUncheckedCount == nChildCount) nParentState = 1; // 全部未选中 else nParentState = 3; // 部分选中 // 设置父节点的状态 m_treeCtrl.SetItemState(hParent, INDEXTOSTATEIMAGEMASK(nParentState), TVIS_STATEIMAGEMASK); // 递归更新父节点的父节点 UpdateParentState(hParent); } 注意:以上代码只是一个框架,实际应用中需要处理更多细节,比如部分选中状态在子节点中的出现(虽然我们不会直接设置子节点为部分选中,但是可能通过其他方式设置)以及性能优化(对于大树,递归可能慢)等。 此外,我们还需要注意,在更新父节点时,如果父节点状态没有变化,我们也可以避免递归更新。 另一种更高效的方法是在更新父节点时,只根据直接子节点的状态来计算,而不需要递归整个子树,因为子节点的状态已经由之前的更新保证了(子节点要么是选中,要么是未选中,不会出现部分选中?不对,因为子节点可能还有子节点,所以子节点可能是部分选中。因此,我们需要递归计算每个节点的状态?但是这样效率较低。因此,我们可以在每个节点上存储当前状态(选中、未选中、部分选中),或者每次更新父节点时,检查其所有子节点的状态(包括子节点是部分选中的情况)。 因此,修改UpdateParentState函数: void CMyDialog::UpdateParentState(HTREEITEM hItem) { HTREEITEM hParent = m_treeCtrl.GetParentItem(hItem); if (hParent == NULL) return; HTREEITEM hChild = m_treeCtrl.GetChildItem(hParent); int nCheckedCount = 0; int nUncheckedCount = 0; int nIndeterminateCount = 0; int nChildCount = 0; while (hChild != NULL) { int nState = GetStateImage(hChild); if (nState == 2) nCheckedCount++; else if (nState == 1) nUncheckedCount++; else if (nState == 3) nIndeterminateCount++; nChildCount++; hChild = m_treeCtrl.GetNextSiblingItem(hChild); } int nParentState; if (nIndeterminateCount > 0 || (nCheckedCount>0 && nUncheckedCount>0)) nParentState = 3; else if (nCheckedCount == nChildCount) nParentState = 2; else // 全部未选中 nParentState = 1; // 如果父节点当前状态和要设置的状态不同,则设置并递归更新 if (GetStateImage(hParent) != nParentState) { m_treeCtrl.SetItemState(hParent, INDEXTOSTATEIMAGEMASK(nParentState), TVIS_STATEIMAGEMASK); // 继续向上更新 UpdateParentState(hParent); } } 这样,当父节点的状态没有变化时,我们就不会继续向上更新,减少不必要的更新。 注意:以上代码没有经过实际测试,可能需要根据实际情况调整。 另外,我们还需要处理通过键盘(空格键)改变状态的情况,处理`TVN_KEYDOWN`消息,当按下空格键时,获取当前选中的项,然后执行和点击状态图标同样的操作。 总结:实现二态(实际是三态)复选框需要自定义状态图像,并处理点击状态图标的事件,递归更新子节点和父节点的状态。 注意:在对话框销毁时,需要销毁图像列表(如果在初始化时没有分离,则需要销毁)。 由于时间关系,这里只提供思路和框架代码。</think>在 MFC控件(CTreeCtrl)中实现二态复选框(实际为三态:未选中、选中、部分选中)需要自定义状态图像和处理逻辑,步骤如下: ### 核心实现步骤 1. **准备状态图像列表** ```cpp CImageList m_stateImageList; m_stateImageList.Create(16, 16, ILC_COLOR32 | ILC_MASK, 3, 1); // 添加三态图像(顺序:未选中、选中、部分选中) m_stateImageList.Add(AfxGetApp()->LoadIcon(IDI_UNCHECKED)); m_stateImageList.Add(AfxGetApp()->LoadIcon(IDI_CHECKED)); m_stateImageList.Add(AfxGetApp()->LoadIcon(IDI_INDETERMINATE)); m_treeCtrl.SetImageList(&m_stateImageList, TVSIL_STATE); ``` 2. **初始化树节点状态** ```cpp HTREEITEM hParent = m_treeCtrl.InsertItem(L"父节点", 0, 0); m_treeCtrl.SetItemState(hParent, INDEXTOSTATEIMAGEMASK(1), TVIS_STATEIMAGEMASK); // 初始未选中 HTREEITEM hChild = m_treeCtrl.InsertItem(L"子节点", 0, 0, hParent); m_treeCtrl.SetItemState(hChild, INDEXTOSTATEIMAGEMASK(2), TVIS_STATEIMAGEMASK); // 初始选中 ``` 3. **处理状态点击事件(ON_NOTIFY)** ```cpp void CMyTreeCtrl::OnClick(NMHDR* pNMHDR, LRESULT* pResult) { CPoint pt; GetCursorPos(&pt); ScreenToClient(&pt); UINT flags = 0; HTREEITEM hItem = HitTest(pt, &flags); if (hItem && (flags & TVHT_ONITEMSTATEICON)) { int state = (GetItemState(hItem, TVIS_STATEIMAGEMASK) >> 12); // 状态切换逻辑:1→2→3→1... int newState = (state % 3) + 1; SetItemState(hItem, INDEXTOSTATEIMAGEMASK(newState), TVIS_STATEIMAGEMASK); // 更新子节点状态 if (ItemHasChildren(hItem)) { UpdateChildrenState(hItem, newState); } // 更新父节点状态 UpdateParentState(hItem); } *pResult = 0; } ``` 4. **递归更新子节点状态** ```cpp void UpdateChildrenState(HTREEITEM hParent, int state) { HTREEITEM hChild = GetChildItem(hParent); while (hChild) { SetItemState(hChild, INDEXTOSTATEIMAGEMASK(state), TVIS_STATEIMAGEMASK); if (ItemHasChildren(hChild)) { UpdateChildrenState(hChild, state); } hChild = GetNextSiblingItem(hChild); } } ``` 5. **更新父节点状态(部分选中逻辑)** ```cpp void UpdateParentState(HTREEITEM hItem) { HTREEITEM hParent = GetParentItem(hItem); if (!hParent) return; BOOL hasChecked = FALSE, hasUnchecked = FALSE; HTREEITEM hChild = GetChildItem(hParent); while (hChild) { int state = (GetItemState(hChild, TVIS_STATEIMAGEMASK) >> 12); if (state == 2) hasChecked = TRUE; else if (state != 3) hasUnchecked = TRUE; // 排除部分选中状态 hChild = GetNextSiblingItem(hChild); } int newState = 3; // 默认部分选中 if (hasChecked && !hasUnchecked) newState = 2; else if (!hasChecked && hasUnchecked) newState = 1; SetItemState(hParent, INDEXTOSTATEIMAGEMASK(newState), TVIS_STATEIMAGEMASK); UpdateParentState(hParent); // 递归向上更新 } ``` ### 关键注意事项 - **状态图像索引**:`INDEXTOSTATEIMAGEMASK(1~3)` 对应图像列表位置 - **部分选中逻辑**:父节点需检查子节点的混合状态 - **性能优化**:大型树结构需避免全树递归,可标记脏节点延迟刷新 - **TVS_CHECKBOXES样式**:需移除默认复选框样式(通过资源视图取消勾选)
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值