This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


August 1998

Microsoft Systems Journal Homepage

Download Aug98Wicked.exe (22KB)

Jeff Prosise is the author of Programming Windows 95 with MFC (Microsoft Press, 1996). He also teaches Visual C++®/MFC programming seminars. For more information, visit http://www.solsem.com.

Lately I've been traveling a lot. When I travel, I meet with other developers of Windows®-based applications and frequently find myself fielding programming questions. In recent months, one question has popped up so many times that I decided I'd better look into it. The question is: how do I create a tree view control that supports drag and drop?
      Now, I confess that I always thought tree view drag and drop was no big deal. After all, a tree view sends its parent a TVN_ BEGINDRAG notification when a dragging operation is begun with the left mouse button, so the control must support drag and drop, right? Well, yes and no. It turns out that there's a lot more to supporting drag and drop in a tree view control than meets the eye. As an experiment, I set out to create a control that mimics the drag and drop behavior of the tree view in Windows Explorer.
      A job that I figured would take an hour or two ended up requiring the better part of two days. I spent much of that time trying to decipher poorly written SDK documentation and figuring out what functions to call and when. The rest of the time was spent adding needed bells and whistles to make the resulting control as user friendly as possible.

Figure 1 TreeDemo
Figure 1 TreeDemo


      The result was the TreeDemo application shown in Figure 1. TreeDemo is a dialog-based MFC application that features a tree view control in its main window. That control is actually an instance of CDragDropTreeCtrl, which is a C++ class derived from MFC's CTreeCtrl class. CDragDropTreeCtrl adds the following features to a standard tree view control:

  • CDragDropTreeCtrl items can be moved using left button drag and drop. Simply grab an item and drop it over the item to which you want to parent it. If the item has subitems, the subitems are moved, too. Thus, an entire branch of the tree can be moved in a single stroke.
  • If the cursor pauses near the top or bottom of a scrollable CDragDropTreeCtrl window during a drag and drop operation, the tree view automatically scrolls while the cursor remains motionless. The first scroll comes after half a second; subsequent scrolls occur at 200 millisecond intervals. Both values are adjustable.
  • If the cursor pauses over an item that has subitems and the item is currently collapsed (that is, its subitems aren't visible), the item is expanded automatically.

      CDragDropTreeCtrl's source code is shown in Figure 2. You can use it as is, modify it, or use it as a base class for classes of your own. The remainder of this column discusses key aspects of CDragDropTreeCtrl's source code and operation.

Drag and Drop Basics
      A tree view control notifies its parent that a drag and drop operation involving the left mouse button has begun by sending it a TVN_BEGINDRAG notification. (A similar notification, TVN_BEGINRDRAG, is sent when drag and drop is initiated with the right mouse button. Both notifications are encapsulated in WM_NOTIFY messages.) This notification is not sent if the tree view control's TVS_DISABLEDRAGDROP style bit is set. Therefore, the first requirement for implementing drag and drop in a tree view is to make sure that this bit is clear. For this reason, CDragDropTreeCtrl::PreCreateWindow zeroes the control's TVS_ DISABLEDRAGDROP bit. Be aware that PreCreateWindow will not be called if a CDragDropTreeCtrl is attached to an existing tree view control. What's the implication? If you attach a CDragDropTreeCtrl to a tree view control created from a dialog template, it's your responsibility to see that the TVS_DISABLEDRAGDROP style is omitted.
      A TVN_BEGINDRAG notification is sent when an item is clicked with the left mouse button and the cursor moves with the button held down. The control does nothing more than send the notification; it's your job to write the code to carry out the drag and drop operation. If the tree view has an associated image list, CImageList will do much of the dirty work for you. Here's what a tree view's parent should do in response to a TVN_BEGINDRAG notification:

  1. Call the control's CTreeCtrl::CreateDragImage function to create a temporary image list containing a drag image. The image will be repeatedly erased and redrawn as the cursor is moved to animate the dragging operation.
  2. Call the image list's CImageList::BeginDrag function, followed by CImageList::DragEnter to draw the first drag image.
  3. Capture the mouse to ensure that the control will continue to receive mouse messages if the cursor moves outside the control window.
      Note that CreateDragImage will fail if you haven't called CTreeCtrl::SetImageList to assign an image list to the control. The CImageList pointer returned by CreateDragImage refers to a different image list, but the fact remains that if you don't assign the control an image list, you can't ask the control to create an image list for drag imaging either.
      CDragDropTreeCtrl's source code is shown in Figure 2. OnBeginDrag handles TVN_BEGINDRAG notifications reflected by the control's parent. It performs all of the steps listed previously. It also caches the handle of the item being dragged in a data member so it can be retrieved when the drop is executed. Here's the pertinent code:

 HTREEITEM hItem = pNMTreeView->itemNew.hItem;
 m_pImageList = CreateDragImage (hItem);
 .
 .
 .
 SetCapture ();
 m_pImageList->BeginDrag (0, hotSpot);
 m_pImageList->DragEnter (this, point);
 m_hDragItem = hItem;
m_pImageList is a CDragDropTreeCtrl member variable that stores a pointer to the temporary CImageList object created by CreateDragImage. It's your responsibility to delete that object when it's no longer needed, so CDragDropTreeCtrl::OnLButtonUp deletes it when the mouse button is released, signifying the end of the drag and drop operation.
      Once dragging has begun, the control must respond to WM_MOUSEMOVE messages by erasing the previous drag image, drawing a new drag image at the current cursor location, and highlighting the item under the cursor to provide visual feedback to the user. In an SDK-style application, you'd have to subclass the control in order to see its WM_MOUSEMOVE messages. In an MFC-based application, you can simply write the message handler into the derived control class, attach an object of that class to the control, and allow MFC to do the subclassing.
      Erasing and redrawing the drag image is easy; CImageList::DragMove will do both. Highlighting the drop target is simple, too. First you call CTreeCtrl::HitTest to find out which item (if any) the cursor is over. Then you call CTreeCtrl::SelectDropTarget to highlight the item. CDragDropTreeCtrl's OnMouseMove function accomplishes all this with two lines of code:

 m_pImageList->DragMove (point);
 •
 •
 •
 HTREEITEM hItem = HighlightDropTarget (point);
HighlightDropTarget is a helper function that turns the cursor location into an HTREEITEM and passes it to SelectDropTarget. To avoid painting problems, the drag image should be hidden before SelectDropTarget is called. That's why HighlightDropTarget sandwiches calls to SelectDropTarget between calls to CImageList::DragShowNolock.
      A drag and drop operation ends when the mouse button is released. Unfortunately, a tree view control doesn't send its parent a notification when this happens. Therefore, you must provide a WM_LBUTTONUP handler that determines which item (if any) the cursor is over, then executes a drop. The message handler should also perform certain housekeeping chores such as erasing the last drag image, releasing the mouse, and deleting the temporary image list. In CDragDropTreeCtrl, these steps are performed by the following statements in OnLButtonUp:

 m_pImageList->DragLeave (this);
 m_pImageList->EndDrag ();
 ::ReleaseCapture ();
 •
 •
 •
 SelectDropTarget (NULL);
 
 delete m_pImageList;
 m_pImageList = NULL;
 
 UINT nFlags;
 HTREEITEM hItem = HitTest (point, &nFlags);
 •
 •
 •
 MoveTree (hItem, m_hDragItem);
      Note the calls to CImageList::DragLeave and CImageList::EndDrag to clean up the drag image, and the call to MoveTree to move the dragged item to its new location. MoveTree is a CDragDropTreeCtrl helper function that moves a specified item and its subitems to another part of the tree. MoveTree calls CopyTree, which in turn relies on a recursive function named CopyChildren to enumerate the item's subitems and copy them to the destination.

Automatic Scrolling
      Tree view drag and drop support isn't complete until you've added automatic scrolling, too. This feature scrolls the tree view window up or down when a cursor carrying a payload pauses near the top or bottom of the window. Without automatic scrolling, it's impossible to select a drop target that's currently scrolled out of view.
      You can see automatic scrolling in action by starting TreeDemo and expanding enough items to cause a vertical scroll bar to appear. Grab an item from the top of the tree and move it down until the cursor is within a few pixels of the control's bottom border. The window will scroll until the last item comes into view or the cursor is moved, whichever comes first. Rest the cursor just inside the window's top border and the tree view will scroll in the opposite direction.
      CDragDropTreeCtrl implements automatic scrolling by setting a timer when the cursor moves within m_nScrollMargin pixels (default=10) of the window's top or bottom border. The relevant code is in OnMouseMove:


 if ((point.y >= 0 && point.y <= m_nScrollMargin) ||
     (point.y >= cy-m_nScrollMargin && point.y<= cy)||
     (...))
     SetTimer (1, m_nDelayInterval, NULL);
Initially, the timer is set to fire after 500 milliseconds. If the cursor moves before the timer fires, the timer is reset. If, however, the cursor remains motionless until the timer interval expires, a WM_TIMER message ensues and CDragDropTreeCtrl::OnTimer scrolls the window by sending it a WS_VSCROLL message:

 if (point.y >= 0 && point.y <= m_nScrollMargin) {
 •
 •
 •
     m_pImageList->DragShowNolock (FALSE);
     SendMessage (WM_VSCROLL, MAKEWPARAM (SB_LINEUP, 0),
                  NULL);
     m_pImageList->DragShowNolock (TRUE);
 •
 •
 •
 }
 else if (point.y >= cy - m_nScrollMargin && point.y
          <= cy) 
 {
 •
 •
 •
     m_pImageList->DragShowNolock (FALSE);
     SendMessage (WM_VSCROLL, MAKEWPARAM (SB_LINEDOWN,
                                          0), NULL);
     m_pImageList->DragShowNolock (TRUE);
 •
 •
 •
 }
      OnTimer also programs the timer to fire again after 200 milliseconds so that subsequent scrolls will happen more quickly. The messages don't stop until the cursor is moved or the tree view is scrolled as far as it possibly can. To adjust the 500 millisecond delay before the first scroll and the 200 millisecond interval between consecutive scrolls, tweak CDragDropTreeCtrl's m_nDelayInterval and m_nScrollInterval data members.

Automatic Branch Expansion
      Consider this scenario: a user grabs a tree view item and drags it to the top or bottom of the window to scroll the desired drop target into view. She finds that the item isn't visible because the branch of the tree in which it resides is collapsed. To ensure that this doesn't happen, CDragDropTreeCtrl automatically expands the tree if the cursor pauses over an item that has subitems that aren't displayed. The same timer logic that drives automatic scrolling is used for automatic branch expansion, too. See CDragDropTreeCtrl::OnMouseMove and CDragDropTreeCtrl::OnTimer for details.

Have a tricky issue dealing with Windows? Send your questions via email to Jeff Prosise: JeffPro@msn.com

From the August 1998 issue of Microsoft Systems Journal.