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

VC++ 樹狀控件節點拖曳完美實作:CXTreeCtrl 完整原始碼解析

     編輯:關於VC++

前言

MFC CTreeCtrl 原生不支援節點拖曳排序。本文實作 CXTreeCtrl 衍生類,完整支援:基本拖曳、誤觸過濾、自動捲動、智能展開、父子節點移動保護。附完整原始碼可直接整合專案。

核心功能架構

功能關鍵技術處理訊息/機制
基本拖曳CreateDragImage, CImageList::BeginDrag/DragEnter/DragMoveTVN_BEGINDRAG, WM_MOUSEMOVE, WM_LBUTTONUP
誤觸過濾時間閾值判斷WM_LBUTTONDOWN 記錄時間, TVN_BEGINDRAG 檢查
自動捲動定時器檢測滑鼠邊緣WM_TIMER, SendMessage(WM_VSCROLL)
智能展開懸停定時器WM_TIMER, HitTest, Expand(TVE_EXPAND)

關鍵成員變數


class CXTreeCtrl : public CTreeCtrl
{
    // 拖曳狀態
    BOOL m_bDragging = FALSE;
    CImageList* m_pDragImage = nullptr;
    HTREEITEM m_hItemDragSrc = nullptr; // 來源項目
    HTREEITEM m_hItemDragDst = nullptr; // 目標項目
    DWORD m_dwDragStart = 0;            // 按下時間

    // 自動捲動
    UINT m_nScrollTimerID = 0;
    UINT m_TimerTicks = 0;

    // 智能展開
    UINT m_nHoverTimerID = 0;
    CPoint m_HoverPoint;
};

核心流程實作

1. 開始拖曳 TVN_BEGINDRAG


void CXTreeCtrl::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
{
    NM_TREEVIEW* pNM = (NM_TREEVIEW*)pNMHDR;
    *pResult = 0;

    // 誤觸過濾:按下未超過 60ms 視為誤觸
    if ((GetTickCount() - m_dwDragStart) < 60)
        return;

    m_hItemDragSrc = pNM->itemNew.hItem;
    m_hItemDragDst = NULL;

    // 建立拖曳影像 (含圖示+文字)
    m_pDragImage = CreateDragImage(m_hItemDragSrc);
    if (!m_pDragImage) return;

    m_bDragging = TRUE;
    m_pDragImage->BeginDrag(0, CPoint(8, 8)); // 熱點偏移

    CPoint pt = pNM->ptDrag;
    ClientToScreen(&pt);
    m_pDragImage->DragEnter(this, pt); // 限制在本控制項內
    SetCapture(); // 捕捉滑鼠

    m_nScrollTimerID = SetTimer(2, 40, NULL); // 啟動自動捲動定時器
}

2. 滑鼠移動 WM_MOUSEMOVE


void CXTreeCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
    // 智能展開:重置懸停定時器
    if (m_nHoverTimerID) KillTimer(m_nHoverTimerID);
    m_nHoverTimerID = SetTimer(1, 800, NULL); // 0.8 秒後展開
    m_HoverPoint = point;

    if (m_bDragging)
    {
        CPoint pt = point;
        CImageList::DragMove(pt); // 更新影像位置

        // 高亮目標項目 (避免殘影)
        CImageList::DragShowNolock(FALSE);
        UINT flags;
        HTREEITEM hHit = HitTest(point, &flags);
        if (hHit)
        {
            SelectDropTarget(hHit); // 高亮顯示
            m_hItemDragDst = hHit;
        }
        CImageList::DragShowNolock(TRUE);

        // 拖至左邊緣 → 移至根節點
        CRect rect;
        GetClientRect(&rect);
        if (point.x < rect.left + 20)
            m_hItemDragDst = NULL;
    }
    CTreeCtrl::OnMouseMove(nFlags, point);
}

3. 結束拖曳 WM_LBUTTONUP


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 = nullptr;
    SelectDropTarget(NULL);

    if (m_hItemDragSrc == m_hItemDragDst) // 未移動
    {
        KillTimer(m_nScrollTimerID);
        return;
    }

    // 展開目標節點
    Expand(m_hItemDragDst, TVE_EXPAND);

    // 防止父節點拖入子節點 (遞迴檢查)
    HTREEITEM hParent = m_hItemDragDst;
    while ((hParent = GetParentItem(hParent)) != NULL)
    {
        if (hParent == m_hItemDragSrc)
        {
            // 父→子:先複製到根節點暫存,再從暫存複製到目標,刪除暫存
            HTREEITEM hTemp = CopyBranch(m_hItemDragSrc, NULL, TVI_LAST);
            HTREEITEM hNew = CopyBranch(hTemp, m_hItemDragDst, TVI_LAST);
            DeleteItem(hTemp);
            SelectItem(hNew);
            KillTimer(m_nScrollTimerID);
            return;
        }
    }

    // 一般移動:複製分支到目標,刪除來源
    HTREEITEM hNew = CopyBranch(m_hItemDragSrc, m_hItemDragDst, TVI_LAST);
    DeleteItem(m_hItemDragSrc);
    SelectItem(hNew);
    KillTimer(m_nScrollTimerID);
}

4. 遞迴複製分支


HTREEITEM CXTreeCtrl::CopyBranch(HTREEITEM hSrc, HTREEITEM hParent, HTREEITEM hAfter)
{
    HTREEITEM hNew = CopyItem(hSrc, hParent, hAfter);
    HTREEITEM hChild = GetChildItem(hSrc);
    while (hChild)
    {
        CopyBranch(hChild, hNew, TVI_LAST); // 遞迴複製子項
        hChild = GetNextSiblingItem(hChild);
    }
    return hNew;
}

HTREEITEM CXTreeCtrl::CopyItem(HTREEITEM hItem, HTREEITEM hParent, HTREEITEM hAfter)
{
    TV_INSERTSTRUCT tv = {0};
    tv.hParent = hParent;
    tv.hInsertAfter = hAfter;
    tv.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT | TVIF_PARAM | TVIF_STATE;
    tv.item.hItem = hItem;
    GetItem(&tv.item); // 取得圖示、文字、資料、狀態

    CString strText = GetItemText(hItem);
    tv.item.pszText = strText.LockBuffer();
    tv.item.cchTextMax = strText.GetLength();

    HTREEITEM hNew = InsertItem(&tv);
    strText.ReleaseBuffer();

    // 複製使用者資料與狀態圖示
    SetItemData(hNew, GetItemData(hItem));
    SetItemState(hNew, GetItemState(hItem, TVIS_STATEIMAGEMASK), TVIS_STATEIMAGEMASK);
    return hNew;
}

5. 定時器處理 WM_TIMER


void CXTreeCtrl::OnTimer(UINT nIDEvent)
{
    // 智能展開
    if (nIDEvent == m_nHoverTimerID)
    {
        KillTimer(m_nHoverTimerID);
        m_nHoverTimerID = 0;
        UINT flags;
        HTREEITEM hItem = HitTest(m_HoverPoint, &flags);
        if (hItem && m_bDragging)
        {
            SelectItem(hItem);
            Expand(hItem, TVE_EXPAND);
        }
    }
    // 自動捲動
    else if (nIDEvent == m_nScrollTimerID)
    {
        m_TimerTicks++;
        CPoint pt;
        GetCursorPos(&pt);
        CRect rect;
        GetClientRect(&rect);
        ClientToScreen(&rect);

        HTREEITEM hFirst = GetFirstVisibleItem();

        // 向上捲動
        if (pt.y < rect.top + 10)
        {
            int speed = 6 - (rect.top + 10 - pt.y) / 20;
            if (m_TimerTicks % max(speed, 1) == 0)
            {
                CImageList::DragShowNolock(FALSE);
                SendMessage(WM_VSCROLL, SB_LINEUP);
                SelectDropTarget(hFirst);
                m_hItemDragDst = hFirst;
                CImageList::DragShowNolock(TRUE);
            }
        }
        // 向下捲動
        else if (pt.y > rect.bottom - 10)
        {
            int speed = 6 - (pt.y - rect.bottom + 10) / 20;
            if (m_TimerTicks % max(speed, 1) == 0)
            {
                CImageList::DragShowNolock(FALSE);
                SendMessage(WM_VSCROLL, SB_LINEDOWN);
                int nVis = GetVisibleCount();
                HTREEITEM hLast = hFirst;
                for (int i = 0; i < nVis - 1; i++)
                    hLast = GetNextVisibleItem(hLast);
                if (hLast)
                {
                    SelectDropTarget(hLast);
                    m_hItemDragDst = hLast;
                }
                CImageList::DragShowNolock(TRUE);
            }
        }
    }
    else
        CTreeCtrl::OnTimer(nIDEvent);
}

6. 誤觸過濾 WM_LBUTTONDOWN


void CXTreeCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    m_dwDragStart = GetTickCount(); // 記錄按下時間
    CTreeCtrl::OnLButtonDown(nFlags, point);
}

對話框整合範例


BOOL CMyDialog::OnInitDialog()
{
    CDialog::OnInitDialog();

    // 影像清單
    m_imgList.Create(IDB_TREE_IMG, 16, 1, RGB(255,255,255));
    m_wndTree.SetImageList(&m_imgList, TVSIL_NORMAL);

    // 建立測試資料
    HTREEITEM hPoem = m_wndTree.InsertItem(_T("唐詩"), 0, 1);
    HTREEITEM hLiBai = m_wndTree.InsertItem(_T("李白"), 0, 1, hPoem);
    m_wndTree.InsertItem(_T("靜夜思"), 0, 1, hLiBai);
    m_wndTree.InsertItem(_T("將進酒"), 0, 1, hLiBai);
    // ... 更多項目

    return TRUE;
}

設計精華總結

  1. 影像清單拖曳:CImageList::DragShowNolock(FALSE/TRUE) 配對使用,消除拖曳殘影
  2. 父子節點保護:遞迴檢查目標是否為來源子孫,採「暫存根節點→複製→刪除暫存」三步驟避免樹結構損壞
  3. 雙定時器分工:ID=1 懸停展開 (800ms),ID=2 邊緣捲動 (40ms),互不干擾
  4. 捲動速度自適應:滑鼠越靠近邊緣捲動越快,體驗更自然
  5. 資料完整複製:TVIF_PARAM|TVIF_STATE 確保使用者資料、核取狀態一併遷移

結語

CXTreeCtrl 展示了 MFC 控制項擴充的經典模式:繼承 + 訊息處理 + 狀態機管理。掌握 CImageList 拖曳 API、HitTest/SelectDropTarget 視覺回饋、定時器驅動的自動捲動/展開,即可為任何樹狀介面加入專業級拖曳體驗。

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