MFC CTreeCtrl 原生不支援節點拖曳排序。本文實作 CXTreeCtrl 衍生類,完整支援:基本拖曳、誤觸過濾、自動捲動、智能展開、父子節點移動保護。附完整原始碼可直接整合專案。
| 功能 | 關鍵技術 | 處理訊息/機制 |
|---|---|---|
| 基本拖曳 | CreateDragImage, CImageList::BeginDrag/DragEnter/DragMove | TVN_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;
};
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); // 啟動自動捲動定時器
}
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);
}
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);
}
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;
}
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);
}
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;
}
CImageList::DragShowNolock(FALSE/TRUE) 配對使用,消除拖曳殘影TVIF_PARAM|TVIF_STATE 確保使用者資料、核取狀態一併遷移CXTreeCtrl 展示了 MFC 控制項擴充的經典模式:繼承 + 訊息處理 + 狀態機管理。掌握 CImageList 拖曳 API、HitTest/SelectDropTarget 視覺回饋、定時器驅動的自動捲動/展開,即可為任何樹狀介面加入專業級拖曳體驗。