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

VC++ 樹狀控件拖放實作全攻略:自動捲動、智能展開與防誤觸

     編輯:關於VC++

MFC 的 CTreeCtrl 雖功能強大,但預設不支援節點拖放。本文將詳細說明如何衍生 CXTreeCtrl 類別,完整實作節點拖放功能,包含基本拖放、防誤觸延遲、拖放過程自動捲動、節點智能展開,以及父節點拖至子節點時的異常防護邏輯。

核心功能特色

  • 基本拖放:處理 TVN_BEGINDRAG、WM_MOUSEMOVE、WM_LBUTTONUP,使用 CImageList 顯示拖曳影像。
  • 防誤觸機制:按下滑鼠後需停留 DRAG_DELAY (預設 60ms) 才啟動拖放,避免手抖誤觸發。
  • 自動捲動:設定 Timer 定期檢測滑鼠是否接近控件上下邊緣,自動送出 WM_VSCROLL 訊息捲動內容。
  • 智能展開:拖曳時滑鼠停留在某節點超過 0.8 秒,自動呼叫 Expand(TVE_EXPAND) 展開該節點。
  • 父子節點保護:偵測將父節點拖入自身子孫節點的情況,透過暫存節點至根節點下的臨時位置,再移至目標位置,最後刪除暫存節點,避免樹狀結構破壞。

關鍵資料成員 (CXTreeCtrl.h)

protected: UINT m_TimerTicks; // 捲動計時器累計 UINT m_nScrollTimerID; // 捲動計時器 ID CPoint m_HoverPoint; // 滑鼠停留座標 UINT m_nHoverTimerID; // 智能展開計時器 ID DWORD m_dwDragStart; // 按下左鍵時間點 BOOL m_bDragging; // 是否正在拖曳 CImageList* m_pDragImage; // 拖曳影像清單 HTREEITEM m_hItemDragS; // 來源節點 HTREEITEM m_hItemDragD; // 目標節點

核心程式碼片段 (CXTreeCtrl.cpp)

1. 開始拖放 (OnBegindrag)

void CXTreeCtrl::OnBegindrag(NMHDR* pNMHDR, LRESULT* pResult) { NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR; *pResult = 0; // 防誤觸:檢查按下時間是否超過門檻 if ((GetTickCount() - m_dwDragStart) < DRAG_DELAY) return; m_hItemDragS = pNMTreeView->itemNew.hItem; m_hItemDragD = NULL; m_pDragImage = CreateDragImage(m_hItemDragS); if (!m_pDragImage) return; m_bDragging = TRUE; m_pDragImage->BeginDrag(0, CPoint(8,8)); CPoint pt = pNMTreeView->ptDrag; ClientToScreen(&pt); m_pDragImage->DragEnter(this, pt); // 限制在本視窗內 SetCapture(); m_nScrollTimerID = SetTimer(2, 40, NULL); // 啟動捲動計時器 }

2. 滑鼠移動 (OnMouseMove) - 高亮目標與智能展開計時

void CXTreeCtrl::OnMouseMove(UINT nFlags, CPoint point) { HTREEITEM hItem; UINT flags; // 重置智能展開計時器 if (m_nHoverTimerID) { KillTimer(m_nHoverTimerID); m_nHoverTimerID = 0; } m_nHoverTimerID = SetTimer(1, 800, NULL); // 0.8 秒後觸發展開 m_HoverPoint = point; if (m_bDragging) { CPoint pt = point; CImageList::DragMove(pt); CImageList::DragShowNolock(FALSE); // 隱藏影像以便 HitTest if ((hItem = HitTest(point, &flags)) != NULL) { SelectDropTarget(hItem); // 高亮潛在放置目標 m_hItemDragD = hItem; } CImageList::DragShowNolock(TRUE); // 拖曳至左邊緣視為放至根節點 CRect rect; GetClientRect(&rect); if (point.x < rect.left + 20) m_hItemDragD = NULL; } CTreeCtrl::OnMouseMove(nFlags, point); }

3. 釋放滑鼠 (OnLButtonUp) - 完成移動/複製與清理

void CXTreeCtrl::OnLButtonUp(UINT nFlags, CPoint point) { CTreeCtrl::OnLButtonUp(nFlags, point); if (!m_bDragging) return; m_bDragging = FALSE; CImageList::DragLeave(this); CImageList::EndDrag(); ReleaseCapture(); delete m_pDragImage; m_pDragImage = NULL; SelectDropTarget(NULL); if (m_hItemDragS == m_hItemDragD) { KillTimer(m_nScrollTimerID); return; } Expand(m_hItemDragD, TVE_EXPAND); // 確保目標節點展開 // 檢查是否為父節點拖入子節點 HTREEITEM htiParent = m_hItemDragD; while ((htiParent = GetParentItem(htiParent)) != NULL) { if (htiParent == m_hItemDragS) { // 父拖入子:透過臨時節點中轉 HTREEITEM htiNewTemp = CopyBranch(m_hItemDragS, NULL, TVI_LAST); HTREEITEM htiNew = CopyBranch(htiNewTemp, m_hItemDragD, TVI_LAST); DeleteItem(htiNewTemp); SelectItem(htiNew); KillTimer(m_nScrollTimerID); return; } } // 一般移動:複製分支至目標,刪除來源 HTREEITEM htiNew = CopyBranch(m_hItemDragS, m_hItemDragD, TVI_LAST); DeleteItem(m_hItemDragS); SelectItem(htiNew); KillTimer(m_nScrollTimerID); }

4. 計時器處理 - 智能展開與自動捲動

void CXTreeCtrl::OnTimer(UINT nIDEvent) { if (nIDEvent == m_nHoverTimerID) // 智能展開 { KillTimer(m_nHoverTimerID); m_nHoverTimerID = 0; HTREEITEM trItem = 0; UINT uFlag = 0; trItem = HitTest(m_HoverPoint, &uFlag); if (trItem && m_bDragging) { SelectItem(trItem); Expand(trItem, TVE_EXPAND); } } else if (nIDEvent == m_nScrollTimerID) // 自動捲動 { m_TimerTicks++; CPoint pt; GetCursorPos(&pt); CRect rect; GetClientRect(&rect); ClientToScreen(&rect); HTREEITEM hItem = GetFirstVisibleItem(); if (pt.y < rect.top + 10) // 靠近上緣 → 向上捲動 { int slowscroll = 6 - (rect.top + 10 - pt.y) / 20; if (0 == (m_TimerTicks % ((slowscroll > 0) ? slowscroll : 1))) { CImageList::DragShowNolock(FALSE); SendMessage(WM_VSCROLL, SB_LINEUP); SelectDropTarget(hItem); m_hItemDragD = hItem; CImageList::DragShowNolock(TRUE); } } else if (pt.y > rect.bottom - 10) // 靠近下緣 → 向下捲動 { int slowscroll = 6 - (pt.y - rect.bottom + 10) / 20; if (0 == (m_TimerTicks % ((slowscroll > 0) ? slowscroll : 1))) { CImageList::DragShowNolock(FALSE); SendMessage(WM_VSCROLL, SB_LINEDOWN); int nCount = GetVisibleCount(); for (int i=0; i

5. 輔助函式:複製單一節點與遞迴複製分支

HTREEITEM CXTreeCtrl::CopyItem(HTREEITEM hItem, HTREEITEM htiNewParent, HTREEITEM htiAfter) { TV_INSERTSTRUCT tvstruct = {0}; CString sText = GetItemText(hItem); tvstruct.item.hItem = hItem; tvstruct.item.mask = TVIF_CHILDREN | TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE; GetItem(&tvstruct.item); tvstruct.item.cchTextMax = sText.GetLength(); tvstruct.item.pszText = sText.LockBuffer(); tvstruct.hParent = htiNewParent; tvstruct.hInsertAfter = htiAfter; tvstruct.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT; HTREEITEM hNewItem = InsertItem(&tvstruct); sText.ReleaseBuffer(); SetItemData(hNewItem, GetItemData(hItem)); SetItemState(hNewItem, GetItemState(hItem, TVIS_STATEIMAGEMASK), TVIS_STATEIMAGEMASK); return hNewItem; } HTREEITEM CXTreeCtrl::CopyBranch(HTREEITEM htiBranch, HTREEITEM htiNewParent, HTREEITEM htiAfter) { HTREEITEM hNewItem = CopyItem(htiBranch, htiNewParent, htiAfter); HTREEITEM hChild = GetChildItem(htiBranch); while (hChild) { CopyBranch(hChild, hNewItem, TVI_LAST); hChild = GetNextSiblingItem(hChild); } return hNewItem; }

使用說明

  1. 將 XTreeCtrl.h/.cpp 加入專案。
  2. 在對話框或視圖類別中,將樹狀控件的變數類型從 CTreeCtrl 改為 CXTreeCtrl。
  3. 確保控件屬性啟用 TVS_HASBUTTONS、TVS_HASLINES、TVS_LINESATROOT 以獲得最佳視覺效果。

圖 1:示例程式執行介面,展示拖放操作。

圖 2:資源編輯器中樹狀控件屬性設定。

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