Visual C++ Developer(Pinnacle Publishing) December ,1997Multi-Select Drag and Drop in a Tree Control
Stephen OwensThe tree control is a powerful tool for displaying data hierarchically, but managing the many features of the CTreeCtrl class can be a time-consuming job. In the September and October issues of Visual C++ Developer, Stephen presented two classes-CBitmapTree and CCheckableTree-that extend and simplify the CTreeCtrl class. In this article, he presents an updated version of the CBitmapTree class, which simplifies the process of adding drag-and-drop capabilities to a tree control. You can even drag and drop multiple selections. Drag and drop is one of the most difficult features to manage and reuse in the CTreeCtrl class. In the interest of providing the greatest amount of flexibility during drag-and-drop operations, the MFC team has made the individual programmer responsible for half a dozen functions. Re-implementing drag and drop each time you need a tree control in your application can become a needlessly daunting task. In this article, I'll present an extension to the CBitmapTree class that simplifies the addition of drag-and-drop capability to your tree control and extends this feature to multi-selected tree items.  This article assumes that you're familiar with many of the concepts and techniques discussed in my September and October articles ("A Tree Control with Multiple Selection and Simpler Image Handling" and "A Tree Control with Three-State Check Boxes and Sophisticated Sorting"). (An updated CCheckableTree [CHECK.ZIP] is also available in the Subscriber Downloads at www.pinpub.com/vcd.)Draggable items and drop targets
There are two main agents in any drag-and-drop operation-the item being dragged and the drop target. In order to support drag and drop in a reusable manner, you need to be able to identify draggable items and their drop targets from within the tree control class rather than the parent window class. Just as a tree image can be designated "expandable" or not, it can be designated "draggable" or not by setting a bit in a member variable called m_draggable. This member variable is used by the functions SetDraggable(), IsDraggable(), SetDropTarget(), and IsDropTarget(). Here is SetDraggable():void CBitmapTree::SetDraggable(int base)
{
  ASSERT(base < (sizeof(m_draggable)*8 -1));  m_draggable|= (1UL << base);
}
Those of you already familiar with CBitmapTree might notice that this function is reably similar to SetExpandable(). The parameter, base, designates the index of an image in the tree control's TVISL_NORMAL image list. This index is converted to a single-bit value and stored in the m_draggable member variable. Because m_draggable is of type long, and is therefore comprised of 32 bits, the base variable must refer to one of the first 32 images in the image list. The IsDraggable() function, not surprisingly, can be used to query the m_draggable variable. IsDraggable() and IsDropTarget() are virtual, so if you need a more complex determination of these two attributes, you might override these functions in a subclass of CBitmapTree. In the sample program available in the Subscriber Downloads at www.pinpub.com/vcd, all of the document nodes are draggable, and all of the folder nodes are drop targets.Getting ready to drag
One additional function must be called before drag-and-drop capabilities will take effect. SetDrag() sets up a callback function that handles the actual moving or copying of tree items once a drag-and-drop operation has been executed successfully. I've supplied a simple callback entitled MoveTreeItemCB() that moves tree items from one place to another in the tree control. Note, however, that this function is quite limited and you'll probably need write your own callback if you do anything out of the ordinary with your control (for example, MoveTreeItemCB() won't behave properly if you use the LPSTR_TEXTCALLBACK macro in place of text for your tree items).  Optionally, you might also supply to SetDrag() a resource identifier of a bitmap to be displayed while tree items are dragged. This bitmap should be in the format shown in Figure 1. The first image is displayed when the drag extends outside of the legal boundaries. The second image is displayed when a single item is dragged. The third item is displayed when multiple items are dragged. If no images are supplied to the SetDrag function, then the default drag image, which is a grayed version of the currently selected item, is displayed.Figure 1: Images for a drag-and-drop operationBeginning a drag
When a drag operation has begun, the parent window of the tree control receives a TVN_BEGINDRAG notification message. You can intercept this message and reflect it to both the parent window and the tree control by adding an ON_NOTIFY_REFLECT_EX notification message to the message map of the CBitmapTree class. Here's the TVN_BEGINDRAG notification handler:BOOL CBitmapTree::OnBeginDrag(NMHDR* pNMHDR, 
                              LRESULT* pResult)
{
  NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
  HTREEITEM h= pNMTreeView->itemNew.hItem;
  int img, simg;
  CPoint point;  if ((m_dragFunction == NULL) || 
      (h == NULL))
    return(FALSE);  ASSERT(GetNormalImageCount());  if (m_multiSelected)
  {
    // turn off selection of nodes 
    // that can't be dragged
    DoForAllSelected(PrepareDrag, NULL, 0);
  }
  else
  {
    // if we have started dragging an unselected item, 
    // we need to complete the selection change before 
    // dragging it
    SelectItem(h);
  }  // can we drag this node?
  GetItemImage(h, img, simg);
  if (!(IsDraggable(h, img)))
    return(FALSE);  // set up drag list
  if (m_ilDrag.m_hImageList != NULL)
    m_dragList= &m_ilDrag;
  else
  {
    m_dragList= CreateDragImage(h);
    if (m_dragList == NULL)
      return(FALSE);
  }  if (m_multiSelected)
    m_dragList->BeginDrag(m_multiDrag, CPoint(0,0));
  else
    m_dragList->BeginDrag(m_docDrag, CPoint(0,0));  MapWindowPoints(NULL, &pNMTreeView->ptDrag, 1);
  m_dragList->DragEnter(NULL, pNMTreeView->ptDrag);  ShowCursor(FALSE);
  SetCapture();  // allow scrolling to occur while dragging  
  SetTimer(EVENT_DRAG_SCROLL, 250, NULL);  *pResult= 0;
  return(FALSE);
}If there are multi-selected nodes in the tree control when OnBeginDrag() is called, you must call PrepareDrag() to turn off the selection of those items that haven't been designated as draggable. (The DoForAllSelected() function, which arranges to call PrepareDrag(), is described in the September article.) Verify that the currently selected item is draggable by calling IsDraggable(). If everything is Okay, initialize the image list that will be displayed during the drag operation, and then call BeginDrag() on this image list. The MSDN documentation says that the second parameter of BeginDrag() is the "coordinates of the starting drag position (typically, the cursor position)." The wording of this statement suggests that the current cursor position, stored in pNMTreeView->ptDrag, should be used, and that, in fact, is what the MSDN sample MFCTREE does. However, more recent MSDN sample apps, CMNCTRLS and TREESCR, use (0, 0) for this parameter, which leads me to believe that it instead refers to the cursor hot spot. This code uses (0,0) also. The last step in OnBeginDrag() is to call DragEnter() to initiate the drag operation. It's worth noting that DragEnter() prevents any drawing within the tree control; if you need to make an update to the tree control after DragEnter() has been called, you must first call DragLeave().Dragging a tree item
While the tree item is being dragged, there are a few visual aspects of the tree control that need to be maintained. We can accomplish these visual changes within the OnMouseMove() handler:void CBitmapTree::OnMouseMove(UINT nFlags, 
                              CPoint point) 
{
  if (m_dragList)
  {
    BOOL target_set = FALSE;
    BOOL has_drag_images 
           = (m_ilDrag.m_hImageList != NULL);
    HTREEITEM target;
    CPoint screen= point;
  
    MapWindowPoints(NULL, &screen, 1);    // which item are we over?
    if ((target= GetItemUnderMouse()) != NULL)
    {
      int img, simg;      if (has_drag_images)
      {
        if (m_multiSelected)
          m_dragList->SetDragCursorImage(m_multiDrag, 
                              CPoint(0,0));
        else
          m_dragList->SetDragCursorImage(m_docDrag, 
                              CPoint(0,0));
      }      while (target)
      {
        GetItemImage(target, img, simg);
        if (IsDropTarget(target, img))
        {
          m_dragList->DragLeave(NULL);
          SelectDropTarget(target);
          m_dragList->DragEnter(NULL, screen);
          target_set= TRUE;
          break;
        }
        target= GetParentItem(target);
      }
    }
    else
    {
      // not over a valid area
      if (has_drag_images)
        m_dragList->SetDragCursorImage(m_xDrag, 
                            CPoint(0,0));
    }
    
    if (!(target_set))
    {
      m_dragList->DragLeave(NULL);
      SelectDropTarget(NULL);
      m_dragList->DragEnter(NULL, screen);
    }
  }  CTreeCtrl::OnMouseMove(nFlags, point);

First, if the mouse is over an invalid area (either outside the tree control or within an area of the tree control that doesn't contain tree items), the drag image should change to show that nothing will happen if the mouse button is released. When the mouse is over a valid area, the drag image should return to its normal state. These visual changes are accomplished by passing the appropriate image to CImageList::SetDragCursorImage(), as demonstrated in Figure 2. Note that if no images were assigned to the tree with a call to SetDrag(), then has_drag_images will be false, and the drag image won't change.Figure 2: Dragging "The Shedding Cycle" from Cats to Snakes: The dragged item is in a dotted rectangle and the target is in reverse video.During the drag operation, you should also maintain a highlight on the appropriate drop target tree item. If the item currently under the mouse is a drop target, then it's highlighted with a call to CTreeCtrl::SelectDropTarget(). If not, then its nearest ancestor is highlighted. If no ancestors are drop targets, then no items are highlighted.Scrolling while dragging
If the drag operation extends past the bounds of the tree control, it would be nice if the control would automatically scroll up or down to reveal more tree items. You can easily accomplish this with an OnTimer() message handler. The timer event EVENT_DRAG_SCROLL is set with a call to SetTimer() in the OnBeginDrag() function. This timer event isn't killed until the OnLButtonUp message handler is called.The OnTimer() message handler is responsible for scrolling up or down when the mouse moves outside the tree control. You might want to take a look at the MSDN sample, TREESCR, that adjusts the speed of the scroll relative to the mouse position, so that moving well outside the control makes the mouse scroll faster.When the drag is over
All good things must come to an end. Drag operations end when the left mouse button is released-therefore, we can catch this event in the OnLButtonUp() message handler:void CBitmapTree::OnLButtonUp(UINT nFlags, 
                              CPoint point) 
{
  if (m_dragList)
  {
    HTREEITEM target;    KillTimer(EVENT_DRAG_SCROLL);    ReleaseCapture();
    m_dragList->EndDrag();
    ShowCursor(TRUE);    m_dragList= NULL;    if ((target= GetDropHilightItem()) != NULL)
    {
      DoForAllSelected(m_dragFunction, NULL, 
                      (LPARAM) target);      SelectDropTarget(NULL);
      SelectItem(target);
    }
    ClearMultiSelect();
  }  CTreeCtrl::OnLButtonUp(nFlags, point);
}
After performing some simple drag-and-drop cleanup, determine whether a drop target is currently highlighted. If so, invoke the callback function, which was set previously by a call to SetDrag(). As I mentioned earlier, you write this function to handle any of the complexities you wish to incorporate in your tree. The simple callback that I've supplied is MoveTreeItemCB(), a good reference when you're ready to write your own callback function.DRAGTREE.ZIP at www.pinpub.com/vcdStephen Owens is a developer at Infinite Software Solutions in Knoxville, TN. He has written software for Home and Garden Television, Greenberg News Networks, and the rock band Metallica. Stephen has played Nerf basketball professionally (that is, while waiting for software to compile) for a number of years. [email protected] find out more about Visual C++ Developer and Pinnacle Publishing, visit their website at http://www.pinpub.com/Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.This article is reproduced from the December 1997 issue of Visual C++ Developer. Copyright 1997, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. Visual C++ Developer is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-788-1900.--------------------------------------------------------------------------------
Send feedback to MSDN.Look here for MSDN Online resources.