MFC `CTreeCtrl` 預設不支援節點拖放。本文實作 `CXTreeCtrl` 繼承 `CTreeCtrl`,完整實現拖放視覺回饋(建立拖放影像)、誤觸防護(時間延遲啟動)、自動捲動(邊緣偵測定時器)、智能展開(懸停延遲展開目標節點)、父節點拖入子節點異常防護。核心訊息處理:`TVN_BEGINDRAG`、`WM_MOUSEMOVE`、`WM_LBUTTONUP`、`WM_TIMER`(雙定時器)。
class CXTreeCtrl : public CTreeCtrl
{
DECLARE_DYNAMIC(CXTreeCtrl)
public:
CXTreeCtrl();
virtual ~CXTreeCtrl();
protected:
// 拖放狀態
CImageList* m_pDragImage = NULL;
HTREEITEM m_hDragItem = NULL; // 被拖曳項目
HTREEITEM m_hDropTarget = NULL; // 目標項目
BOOL m_bDragging = FALSE;
DWORD m_dwDragStartTime = 0; // 按下時間,用於誤觸延遲
// 自動捲動定時器
UINT m_nScrollTimer = 0;
int m_nScrollTicks = 0;
CPoint m_ptLastMouse;
// 智能展開定時器
UINT m_nHoverTimer = 0;
HTREEITEM m_hHoverItem = NULL;
static const UINT DRAG_DELAY_MS = 60; // 誤觸延遲 60ms
static const UINT SCROLL_TIMER_ID = 1;
static const UINT HOVER_TIMER_ID = 2;
static const int SCROLL_MARGIN = 20; // 邊緣 20px 觸發捲動
afx_msg void OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnTimer(UINT_PTR nIDEvent);
DECLARE_MESSAGE_MAP()
};
void CXTreeCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_TREEVIEW* pNM = (NM_TREEVIEW*)pNMHDR;
*pResult = 0;
// 1. 誤觸防護:按下時間太短視為點擊而非拖曳
if (GetTickCount() - m_dwDragStartTime < DRAG_DELAY_MS) return;
m_hDragItem = pNM->itemNew.hItem;
if (!m_hDragItem) return;
// 2. 建立拖放影像(預設:項目圖示 + 文字)
m_pDragImage = CreateDragImage(m_hDragItem);
if (!m_pDragImage) return;
// 3. 設定熱點(影像左上角偏移),開始拖放
m_pDragImage->BeginDrag(0, CPoint(8, 8));
CPoint pt = pNM->ptDrag;
ClientToScreen(&pt);
m_pDragImage->DragEnter(NULL, pt); // NULL = 桌面座標
// 4. 捕捉滑鼠,進入拖放狀態
SetCapture();
m_bDragging = TRUE;
m_hDropTarget = NULL;
// 5. 啟動自動捲動定時器
m_nScrollTimer = SetTimer(SCROLL_TIMER_ID, 50, NULL);
m_nScrollTicks = 0;
}
void CXTreeCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
if (!m_bDragging) { CTreeCtrl::OnMouseMove(nFlags, point); return; }
ClientToScreen(&point);
// 1. 更新拖放影像位置
m_pDragImage->DragMove(point);
m_pDragImage->DragShowNolock(FALSE); // 暫時隱藏影像以便 HitTest
// 2. 計算目標項目
CPoint ptClient = point; ScreenToClient(&ptClient);
UINT flags;
HTREEITEM hHit = HitTest(ptClient, &flags);
if (hHit && (flags & TVHT_ONITEM))
{
// 高亮潛在放置目標
SelectDropTarget(hHit);
m_hDropTarget = hHit;
// 3. 智能展開:啟動懸停定時器
if (hHit != m_hHoverItem)
{
m_hHoverItem = hHit;
if (m_nHoverTimer) KillTimer(m_nHoverTimer);
m_nHoverTimer = SetTimer(HOVER_TIMER_ID, 800, NULL); // 800ms 後展開
}
}
else
{
SelectDropTarget(NULL);
m_hDropTarget = NULL;
if (m_nHoverTimer) { KillTimer(m_nHoverTimer); m_nHoverTimer = 0; m_hHoverItem = NULL; }
}
m_pDragImage->DragShowNolock(TRUE); // 恢復影像顯示
m_ptLastMouse = point;
CTreeCtrl::OnMouseMove(nFlags, point);
}
void CXTreeCtrl::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == SCROLL_TIMER_ID && m_bDragging)
{
// 自動捲動:檢查滑鼠是否接近客戶區邊緣
CRect rc; GetClientRect(&rc);
CPoint pt = m_ptLastMouse; ScreenToClient(&pt);
int nScroll = 0;
if (pt.y < SCROLL_MARGIN) nScroll = -1; // 向上
else if (pt.y > rc.bottom - SCROLL_MARGIN) nScroll = 1; // 向下
if (nScroll != 0)
{
// 捲動速度隨距離邊緣遠近調整(簡化:固定 1 行)
SendMessage(WM_VSCROLL, MAKEWPARAM(SB_LINEUP + (nScroll > 0), 0), 0);
}
}
else if (nIDEvent == HOVER_TIMER_ID && m_bDragging)
{
// 智能展開:懸停超時,展開目標節點
if (m_hHoverItem)
{
Expand(m_hHoverItem, TVE_EXPAND);
KillTimer(m_nHoverTimer); m_nHoverTimer = 0;
}
}
CTreeCtrl::OnTimer(nIDEvent);
}
void CXTreeCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
if (!m_bDragging) { CTreeCtrl::OnLButtonUp(nFlags, point); return; }
// 1. 清理拖放資源
m_pDragImage->DragLeave(this);
m_pDragImage->EndDrag();
delete m_pDragImage; m_pDragImage = NULL;
ReleaseCapture();
SelectDropTarget(NULL);
if (m_nScrollTimer) { KillTimer(m_nScrollTimer); m_nScrollTimer = 0; }
if (m_nHoverTimer) { KillTimer(m_nHoverTimer); m_nHoverTimer = 0; }
m_bDragging = FALSE;
// 2. 執行移動/複製邏輯
if (m_hDropTarget && m_hDropTarget != m_hDragItem)
{
// 防護:不能將父節點移至自己的子節點下
HTREEITEM hParent = m_hDropTarget;
while (hParent)
{
if (hParent == m_hDragItem) { AfxMessageBox(_T("不能將父節點移至子節點")); return; }
hParent = GetParentItem(hParent);
}
// 取得項目文字與資料
CString strText = GetItemText(m_hDragItem);
DWORD_PTR dwData = GetItemData(m_hDragItem);
int nImage = GetItemImage(m_hDragItem), nSelImage = GetItemSelectedImage(m_hDragItem);
// 複製到目標節點下
HTREEITEM hNew = InsertItem(strText, nImage, nSelImage, m_hDropTarget, TVI_LAST);
SetItemData(hNew, dwData);
// 遞歸複製子節點
CopySubtree(m_hDragItem, hNew);
// 刪除原節點
DeleteItem(m_hDragItem);
}
CTreeCtrl::OnLButtonUp(nFlags, point);
}
void CXTreeCtrl::CopySubtree(HTREEITEM hSrc, HTREEITEM hDestParent)
{
HTREEITEM hChild = GetChildItem(hSrc);
while (hChild)
{
CString str = GetItemText(hChild);
DWORD_PTR data = GetItemData(hChild);
int img = GetItemImage(hChild), selImg = GetItemSelectedImage(hChild);
HTREEITEM hNew = InsertItem(str, img, selImg, hDestParent, TVI_LAST);
SetItemData(hNew, data);
CopySubtree(hChild, hNew); // 遞歸
hChild = GetNextItem(hChild, TVGN_NEXT);
}
}
void CXTreeCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
m_dwDragStartTime = GetTickCount(); // 記錄按下時刻
CTreeCtrl::OnLButtonDown(nFlags, point);
}
// 對話方塊資源:Tree Control 屬性勾選 "Has Buttons", "Has Lines", "Lines at Root"
// 類別向導關聯變數 m_wndTree,類型改為 CXTreeCtrl
BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 填入測試資料
HTREEITEM hRoot = m_wndTree.InsertItem(_T("我的電腦"));
HTREEITEM hC = m_wndTree.InsertItem(_T("本機磁碟 (C:)"), hRoot);
HTREEITEM hD = m_wndTree.InsertItem(_T("本機磁碟 (D:)"), hRoot);
m_wndTree.InsertItem(_T("Windows"), hC);
m_wndTree.InsertItem(_T("Program Files"), hC);
m_wndTree.Expand(hRoot, TVE_EXPAND);
return TRUE;
}

拖曳節點時顯示半透明影像,移至邊緣自動捲動,懸停目標節點 0.8 秒自動展開,禁止父移入子,放開滑鼠完成移動並保留子樹結構。
`CXTreeCtrl` 以約 150 行核心代碼實現企業級樹狀拖放體驗。關鍵設計:雙定時器分離捲動與展開邏輯、DragShowNolock 避免影像殘留、遞歸複製子樹保證結構完整、時間延遲濾除誤觸。可直接納入共用控制項庫,支援 `TVS_DISABLEDRAGDROP` 樣式切換啟用/禁用。