Implementing Drag and Drop for a Tree View Item

Now that you have a tree view control that can be expanded and collapsed, it would be neat if the user could pick up one of the items and drag it to a new location. The tree view control has some built-in functions that facilitate this operation. When processing a drag operation for a tree view item, an application typically does the following:

  1. Processes the start of the drag
  2. Processes the dragging
  3. Processes the drop

An application processes the start of the drag (picking up the item) in the window procedure of the parent window by using the TVN_BEGINDRAG notification (if the user is dragging with the left mouse button) or the TVN_BEGINRDRAG notification (if the user is dragging with the right mouse button). These notifications are sent through a WM_NOTIFY message. The following sample code creates a drag image, captures the mouse, and sets a Boolean flag to signal that dragging is occurring:

case WM_NOTIFY:
switch(((LPNMHDR)lParam)->code)
{
case TVN_BEGINDRAG:
// The user wants to drag an item. Call the drag handler.
BeginDrag (hWndTreeView, (NM_TREEVIEW *)lParam);
// Save the dragged item information.
tvI = ((NM_TREEVIEW *)lParam)->itemNew;
// Get a handle to the drag object.
hDragItem = tvI.hItem;
break;

default:
break;
}
break;
§
VOID BeginDrag (HWND hwndTree, NM_TREEVIEW *lItem)
{
HIMAGELIST hIml;
RECT rcl;

// Create an image to use for dragging.
hIml = TreeView_CreateDragImage (hwndTree, lItem->itemNew.hItem);

// Get the bounding rectangle of the item being dragged.
TreeView_GetItemRect (hwndTree, lItem->itemNew.hItem, &rcl, TRUE);

// Start dragging the image.
ImageList_BeginDrag (hIml, 0, lItem->ptDrag.x, lItem->ptDrag.y);

// Hide the cursor.
ShowCursor (FALSE);

// Capture the mouse.
SetCapture (GetParent (hwndTree));

// Set a global flag that tells whether dragging is occurring.
g_fDragging = TRUE;
}

The MFCTREE sample handles the drag-and-drop operation through a virtual function mapped to the WindowProc function:

// Handle the WM_NOTIFY::TVN_BEGINDRAG notification.
LRESULT CMfctreeView::WindowProc (UINT message, WPARAM wParam,
LPARAM lParam)
{
TV_ITEM tvI;
if (message == WM_NOTIFY)
{
if (((LPNMHDR)lParam)->code == TVN_BEGINDRAG)
{
BeginDrag ((NM_TREEVIEW *)lParam);
tvI = ((NM_TREEVIEW *)lParam)->itemNew;

// Get a handle to the drag object.
m_hDragItem = tvI.hItem;
}
}
return CView::WindowProc (message, wParam, lParam);
}

VOID CMfctreeView::BeginDrag (NM_TREEVIEW *lItem)
{
CImageList *CImage;

// Create an image to use for dragging.
CImage = m_TreeCtl.CreateDragImage (lItem->itemNew.hItem);

// Start dragging the image.
CImage->BeginDrag (0, lItem->ptDrag);

// Hide the cursor.
ShowCursor (FALSE);

SetCapture ();
m_fDragging = TRUE;
}

The application processes the dragging operation by capturing the mouse and monitoring the WM_MOUSEMOVE messages. In a typical drag-and-drop scenario, the image appears to be dragged because the cursor is changed to the image of the item being dragged.

VOID CMfctreeView::OnMouseMove (UINT nFlags, CPoint point)
{
HTREEITEM hTarget;
UINT flags;

if (m_fDragging)
{
// Drag the item to the current mouse position.
m_ImageList.DragMove (point);

flags = TVHT_ONITEM;
// If the cursor is on an item, highlight it as the drop target.
if ((hTarget = m_TreeCtl.HitTest (point, &flags)) != NULL)
m_TreeCtl.SelectDropTarget (hTarget);
}

CView::OnMouseMove (nFlags, point);
}

When the user finishes dragging the item, the application can look for the WM_LBUTTONUP message. At this point, the currently selected item is recorded, the mouse is released, and the cursor is restored to the previous state. This is also the point at which you want to reset the parentage of the item and reset any internal structures that are keeping track of your tree items. Also, remember to reset the drop highlight item. During the drag operation, the drop highlight item changes dynamically as the user moves the mouse. When the item is dropped, you need to set the drop highlight back to NULL, or you will end up with two items that both appear selected, because selected items and drop highlight items are painted the same way. This is remedied by another call to TreeView_SelectItem, passing NULL for the hItem parameter.

case WM_LBUTTONUP:
// If dragging, stop it.
if (g_fDragging)
{
// Process the item drop.
DropItem (hDragItem, hWndTreeView);

// Inform the image list that dragging has stopped.
ImageList_EndDrag ();

// Release the mouse capture.
ReleaseCapture ();

// Show the cursor.
ShowCursor (TRUE);

// Reset the global Boolean flag to a nondragging state.
g_fDragging = FALSE;
}
break;

// Function that processes the item drop
VOID DropItem (HTREEITEM hDragItem, HWND hwnd)
{
HTREEITEM hParent, hNewItem, hTarget;
TV_ITEM tvTarget;
int index;

// Get the handle to the drop target.
hTarget = TreeView_GetDropHilight (hwnd);

// Get the parent of the drop target.
hParent = TreeView_GetParent (hwnd, hTarget);

// Get the image information.
tvTarget.hItem = hTarget;
tvTarget.mask = TVIF_IMAGE;
TreeView_GetItem (hwnd, &tvTarget);

// Get the index into the structure containing the text for the items.
for (index = 0; index < NUM_HOUSES; index++)
{
if (rgHouseInfo[index].hItem == hDragItem)
break;
}

if (index == NUM_HOUSES)
index—;

// Reinsert the new item.
hNewItem = AddOneItem (hParent, rgHouseInfo[index].szAddress,
hTarget, tvTarget.iImage, hwnd);

// Delete the dragged item.
TreeView_DeleteItem(hwnd, hDragItem);

// Reset the drop target to NULL.
TreeView_SelectDropTarget (hwnd, (HTREEITEM)NULL);
}

As you can see, processing a drag-and-drop operation for a tree view control is not at all difficult, so you really won't have any excuse for not supporting it.