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

VC++ 樹狀控制項完美拖放實作:CXTreeCtrl 支援智能展開、自動捲動、誤觸防護

     編輯:VC 教學

前言

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

二、啟動拖放:TVN_BEGINDRAG


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

三、拖放過程:WM_MOUSEMOVE


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

四、自動捲動與智能展開:WM_TIMER


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

五、結束拖放:WM_LBUTTONUP


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` 樣式切換啟用/禁用。

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