在專案管理、權限設定、目錄選擇等場景,常需「父節點全選→子節點全選、部分選→父節點顯示中間狀態」的三態邏輯。本文提供基於 CTreeCtrl 衍生的 CMutiTreeCtrl 完整實作,特色:
TVS_CHECKBOXES 風格與狀態影像清單,不需自繪,效能佳且支援節點圖示同時顯示。
執行效果:荊門市(全選)→湖北省(部分選)→江蘇省(未選)。
狀態影像清單:索引 0=無方塊、1=未選、2=部分選、3=全選,建議 13x13 像素。
TVS_CHECKBOXES、TVS_HASBUTTONS、TVS_HASLINES、TVS_LINESATROOT。MutiTreeCtrl.h/.cpp 至專案。#include "MutiTreeCtrl.h",將控件變數類型從 CTreeCtrl 改為 CMutiTreeCtrl (如 m_TripleTree)。CImageList 成員:m_imgList (節點圖示)、m_imgState (三態核取方塊)。OnInitDialog 初始化影像清單並關聯:
m_imgState.Create(IDB_BITMAP_STATE, 13, 1, RGB(255,255,255)); // 13x13 三態圖
m_imgList.Create(IDB_BITMAP_LIST, 16, 1, RGB(255,255,255)); // 16x16 節點圖示
m_TripleTree.SetImageList(&m_imgList, TVSIL_NORMAL);
m_TripleTree.SetImageList(&m_imgState, TVSIL_STATE); // 關鍵:狀態影像清單
m_TripleTree.SetItemState(hRoot, INDEXTOSTATEIMAGEMASK(0), TVIS_STATEIMAGEMASK);
class CMutiTreeCtrl : public CTreeCtrl
{
public:
CMutiTreeCtrl();
virtual ~CMutiTreeCtrl();
// 重載:bSearch=TRUE 表示使用者操作需遍歷子節點;FALSE 為程式設值僅同步父/兄
virtual BOOL SetItemState(HTREEITEM hItem, UINT nState, UINT nMask, BOOL bSearch=TRUE);
protected:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnStateIconClick(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
DECLARE_MESSAGE_MAP()
private:
UINT m_uFlags; // HitTest 旗標
void TravelChild(HTREEITEM hItem, int nState); // 遞迴設定子樹
void TravelSiblingAndParent(HTREEITEM hItem, int nState); // 遞迴同步父/兄
};
BOOL CMutiTreeCtrl::SetItemState(HTREEITEM hItem, UINT nState, UINT nMask, BOOL bSearch)
{
BOOL bRet = CTreeCtrl::SetItemState(hItem, nState, nMask);
UINT iState = nState >> 12; // 取得狀態影像索引 (1,2,3)
if (iState != 0)
{
if (bSearch) TravelChild(hItem, iState); // 同步子節點
TravelSiblingAndParent(hItem, iState); // 同步父/兄節點
}
return bRet;
}
void CMutiTreeCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
HTREEITEM hItem = HitTest(point, &m_uFlags);
if (m_uFlags & TVHT_ONITEMSTATEICON)
{
UINT nState = (GetItemState(hItem, TVIS_STATEIMAGEMASK) >> 12);
nState = (nState == 3) ? 1 : 3; // 全選→未選,未選→全選
SetItemState(hItem, INDEXTOSTATEIMAGEMASK(nState), TVIS_STATEIMAGEMASK);
}
CTreeCtrl::OnLButtonDown(nFlags, point);
}
void CMutiTreeCtrl::OnStateIconClick(NMHDR* pNMHDR, LRESULT* pResult)
{
*pResult = (m_uFlags & TVHT_ONITEMSTATEICON) ? 1 : 0; // 1=已處理,阻止預設行為
}
void CMutiTreeCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if (nChar == VK_SPACE) // 0x20
{
HTREEITEM hItem = GetSelectedItem();
if (hItem)
{
UINT nState = (GetItemState(hItem, TVIS_STATEIMAGEMASK) >> 12);
if (nState != 0)
{
nState = (nState == 3) ? 1 : 3;
SetItemState(hItem, INDEXTOSTATEIMAGEMASK(nState), TVIS_STATEIMAGEMASK);
}
}
}
else CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
void CMutiTreeCtrl::TravelChild(HTREEITEM hItem, int nState)
{
HTREEITEM hChild = GetChildItem(hItem);
while (hChild)
{
// 只有原本有核取方塊 (狀態!=0) 的節點才同步
if ((GetItemState(hChild, TVIS_STATEIMAGEMASK) >> 12) != 0)
CTreeCtrl::SetItemState(hChild, INDEXTOSTATEIMAGEMASK(nState), TVIS_STATEIMAGEMASK);
TravelChild(hChild, nState); // 遞迴處理孫節點
// 處理兄弟節點
HTREEITEM hSibling = GetNextSiblingItem(hChild);
while (hSibling)
{
if ((GetItemState(hSibling, TVIS_STATEIMAGEMASK) >> 12) != 0)
CTreeCtrl::SetItemState(hSibling, INDEXTOSTATEIMAGEMASK(nState), TVIS_STATEIMAGEMASK);
TravelChild(hSibling, nState);
hSibling = GetNextSiblingItem(hSibling);
}
hChild = GetNextSiblingItem(hChild); // 此行邏輯需注意,原碼為單層遞迴,此處修正為完整遍歷
}
}
註: 原始碼遞迴邏輯略有疏漏,建議改用標準樹遍歷 (先處理第一個子節點遞迴,再處理下一個兄弟節點遞迴) 以確保完整覆蓋。
void CMutiTreeCtrl::TravelSiblingAndParent(HTREEITEM hItem, int nState)
{
HTREEITEM hParent = GetParentItem(hItem);
if (!hParent) return;
int nSiblingState = nState;
// 檢查下方兄弟
HTREEITEM hNext = GetNextSiblingItem(hItem);
while (hNext)
{
nSiblingState = (GetItemState(hNext, TVIS_STATEIMAGEMASK) >> 12);
if (nSiblingState != nState && nSiblingState != 0) break;
hNext = GetNextSiblingItem(hNext);
}
// 檢查上方兄弟
if (nSiblingState == nState)
{
HTREEITEM hPrev = GetPrevSiblingItem(hItem);
while (hPrev)
{
nSiblingState = (GetItemState(hPrev, TVIS_STATEIMAGEMASK) >> 12);
if (nSiblingState != nState && nSiblingState != 0) break;
hPrev = GetPrevSiblingItem(hPrev);
}
}
if (nSiblingState == nState || nSiblingState == 0)
{
// 所有可見兄弟狀態一致 → 父節點同步
int nParentState = (GetItemState(hParent, TVIS_STATEIMAGEMASK) >> 12);
if (nParentState != 0)
CTreeCtrl::SetItemState(hParent, INDEXTOSTATEIMAGEMASK(nState), TVIS_STATEIMAGEMASK);
TravelSiblingAndParent(hParent, nState); // 遞迴向上
}
else
{
// 兄弟狀態不一 → 父節點及以上祖先設為「部分選」(2)
HTREEITEM hAncestor = hParent;
while (hAncestor)
{
int nAncState = (GetItemState(hAncestor, TVIS_STATEIMAGEMASK) >> 12);
if (nAncState != 0)
CTreeCtrl::SetItemState(hAncestor, INDEXTOSTATEIMAGEMASK(2), TVIS_STATEIMAGEMASK);
hAncestor = GetParentItem(hAncestor);
}
}
}
TVSIL_STATE 影像清單,系統自動處理核取方塊繪製與點擊區域。TVSIL_NORMAL 仍可顯示節點圖示 (如資料夾、檔案)。SetItemState(..., FALSE) 可單純設定節點而不觸發子樹遞迴,效能更好。完整專案碼可從 VC 知識庫下載。如有問題歡迎來信交流。