首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

三態核取方塊樹狀控件 (Tri-State Checkbox Tree) 完整實作:父子節點狀態同步、鍵盤支援與圖示共存

     編輯:關於VC++

在專案管理、權限設定、目錄選擇等場景,常需「父節點全選→子節點全選、部分選→父節點顯示中間狀態」的三態邏輯。本文提供基於 CTreeCtrl 衍生的 CMutiTreeCtrl 完整實作,特色:

  • 使用控件原生 TVS_CHECKBOXES 風格與狀態影像清單,不需自繪,效能佳且支援節點圖示同時顯示。
  • 滑鼠點擊、空白鍵、程式設值皆自動遞迴同步父/子/兄弟節點狀態。
  • 支援隱藏特定節點的核取方塊 (設狀態為 0)。
  • 程式碼結構清晰,易整合既有專案。

執行效果:荊門市(全選)→湖北省(部分選)→江蘇省(未選)。

狀態影像清單:索引 0=無方塊、1=未選、2=部分選、3=全選,建議 13x13 像素。

六步驟快速整合

  1. 建立對話框專案,放置 Tree Control,設屬性 TVS_CHECKBOXES、TVS_HASBUTTONS、TVS_HASLINES、TVS_LINESATROOT。
  2. 加入 MutiTreeCtrl.h/.cpp 至專案。
  3. 對話框標頭檔 #include "MutiTreeCtrl.h",將控件變數類型從 CTreeCtrl 改為 CMutiTreeCtrl (如 m_TripleTree)。
  4. 對話框類別新增兩個 CImageList 成員:m_imgList (節點圖示)、m_imgState (三態核取方塊)。
  5. 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); // 關鍵:狀態影像清單
  1. 編譯執行,即可運作。若需隱藏根節點核取方塊:
m_TripleTree.SetItemState(hRoot, INDEXTOSTATEIMAGEMASK(0), TVIS_STATEIMAGEMASK);

核心架構 (MutiTreeCtrl.h)

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); // 遞迴同步父/兄 };

關鍵實作細節 (MutiTreeCtrl.cpp)

1. 重載 SetItemState — 狀態變更的總入口

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

2. 滑鼠點擊狀態圖示 — 切換 1↔3 並觸發同步

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=已處理,阻止預設行為 }

3. 鍵盤空白鍵支援

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

4. 遞迴同步子節點 — TravelChild

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); // 此行邏輯需注意,原碼為單層遞迴,此處修正為完整遍歷 } }

註: 原始碼遞迴邏輯略有疏漏,建議改用標準樹遍歷 (先處理第一個子節點遞迴,再處理下一個兄弟節點遞迴) 以確保完整覆蓋。

5. 遞迴同步父/兄節點 — TravelSiblingAndParent

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 知識庫下載。如有問題歡迎來信交流。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved