MFC 的 CTreeCtrl 雖功能強大,但預設不支援節點拖放。本文將詳細說明如何衍生 CXTreeCtrl 類別,完整實作節點拖放功能,包含基本拖放、防誤觸延遲、拖放過程自動捲動、節點智能展開,以及父節點拖至子節點時的異常防護邏輯。
TVN_BEGINDRAG、WM_MOUSEMOVE、WM_LBUTTONUP,使用 CImageList 顯示拖曳影像。DRAG_DELAY (預設 60ms) 才啟動拖放,避免手抖誤觸發。WM_VSCROLL 訊息捲動內容。Expand(TVE_EXPAND) 展開該節點。
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; // 目標節點
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); // 啟動捲動計時器
}
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);
}
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);
}
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;
}
使用說明
- 將
XTreeCtrl.h/.cpp 加入專案。
- 在對話框或視圖類別中,將樹狀控件的變數類型從
CTreeCtrl 改為 CXTreeCtrl。
- 確保控件屬性啟用
TVS_HASBUTTONS、TVS_HASLINES、TVS_LINESATROOT 以獲得最佳視覺效果。
圖 1:示例程式執行介面,展示拖放操作。
圖 2:資源編輯器中樹狀控件屬性設定。