tree drag & drop
(level : medium)


Contents

Introduction
CTreeView based application
  Simple drag & drop
    Initialization
    Execution
    Exit
    Result
  Cancellation
  Expanding / collapsing items
  Scrolling
CTreeCtrl based application
Reference


Introduction

Moving items inside a "tree" control is not excessively complicated with MFC. However, it is necessary as often to know some subtleties that are not always brought to light in the examples, and can lead to spend time in the documentation or on the internet. This was the case for me, that's why I wrote this article as well as the short sample (377 Kb) that comes with it.


the "camera 3" item being moved

The above zip file actually contains 2 Visual C++ 6 projects : in the first one the tree is managed by a class derived from CTreeView (previous picture), in the other one it's a CTreeCtrl placed in a CFormView. The two implementations have a lot of common points, and some differences that will be detailed a little further.


CTreeView based application

This version is located in the DragDropView directory. Apart from the addition to the resources of the IDB_SCENE_TREE bitmap that contains some images for the tree items, all the modifications made in the MDI application after it was created by the wizard are concentrated in the CDragDropView class.


IDB_SCENE_TREE

Beside drag & drop, this class (derived from CTreeView) deals with the following operations :

- it sets the styles of the tree in the PreCreateWindow function. Be careful, this has to be done after the call to the base class with
CTreeView::PreCreateWindow(cs);
BOOL CDragDropView::PreCreateWindow(CREATESTRUCT& cs)
{
  BOOL Res  = CTreeView::PreCreateWindow(cs);
  cs.style |= TVS_FULLROWSELECT | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_SHOWSELALWAYS;
  return Res;
}

- it associates the bitmap IDB_SCENE_TREE with the tree through the lines :

in CDragDropView::CDragDropView()
  m_imgList.Create(IDB_SCENE_TREE,16,1,RGB(255,255,255));
in CDragDropView::OnInitialUpdate()
  CTreeCtrl& Tree = GetTreeCtrl();
  Tree.SetImageList(&m_imgList,TVSIL_NORMAL);
in CDragDropView::~CDragDropView() : don't forget to release the resource
  m_imgList.DeleteImageList();

- it inserts items in the tree at creation time (CDragDropView::OnInitialUpdate()) so that you can test drag & drop.


Simple drag & drop

The code for a "simple" drag & drop (as it is described in the documentation) is made of 3 parts :

- the initialization : when the user begins a drag & drop (he clicks an item and moves the mouse without releasing the left button), the tree sends a TVN_BEGINDRAG message, handled by the CDragDropView::OnBegindrag function.

- the execution : the drag & drop goes on as long as the user keeps the mouse button held down. The moves are generally handled in the OnMouseMove function (in response to the WM_MOUSEMOVE messages).

- the exit : the operation terminates when the left button is released, that is to say when a WM_LBUTTONUP event is received.


Some variables are necessary :
bool              m_boDragging;
HTREEITEM         m_hDragItem;
HTREEITEM         m_hDragTarget;
CImageList*       m_pDragImgList;
m_boDragging tells us if a drag & drop operation is running. In the opposite case, the code added to handle the WM_MOUSEMOVE and WM_LBUTTONUP events will not be processed. This variable is set to true in OnBeginDrag, and to false in OnLButtonUp.

m_hDragItem contains the handle of the item being moved. Its value is initialized in OnBeginDrag.

m_hDragTarget stores the handle of the item selected at any moment to be the "target" of the drag & drop, that is to say to become the new parent of the moved item. On the picture in the introduction, "camera 3" is the item being moved and "camera 6" is the current target. m_hDragTarget is modified according to the mouse moves.

m_pDragImgList is a CImageList (like the one, seen before, that contains the images the tree can use), whose goal is to draw a sprite of the item being moved, at the position of the mouse.


Initialization

The initialization code is the following :

void CDragDropView::OnBegindrag(NMHDR* pNMHDR, LRESULT* pResult) 
{
  NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
  HTREEITEM    hItem       = pNMTreeView->itemNew.hItem;
  if(!hItem) return;
  GetTreeCtrl().SelectItem(hItem);

  m_hDragItem    = hItem;
  m_pDragImgList = GetTreeCtrl().CreateDragImage(hItem);
  if(!m_pDragImgList) return;
  
  m_pDragImgList->BeginDrag(0,CPoint(0,0));
  m_pDragImgList->DragEnter(this,pNMTreeView->ptDrag);
  m_boDragging = true;

  ShowCursor(false);
  SetCapture();
  m_hDragTarget = NULL;

  SetTimer(1,25,NULL);
  *pResult = 0;
}

It is very similar to what can be found in the documentation, except for the SetTimer call which will be explained in the 'scrolling' paragraph.
Note the SetCapture call, that allows the view containing the tree to continue to receive the messages from the mouse even when this one is not above it.


Execution

The moving of the mouse is handled this way :

void CDragDropView::OnMouseMove(UINT nFlags, CPoint point) 
{
  if(m_boDragging)
  {
    // highlight target

    CTreeCtrl& Tree = GetTreeCtrl();
    TVHITTESTINFO tvHit;
    tvHit.pt = point;
    HTREEITEM hTarget = Tree.HitTest(&tvHit);

    if(hTarget)
    {
      if(hTarget != m_hDragTarget)
      {                                                     // this test avoids flickering
        m_pDragImgList->DragShowNolock(false);
        Tree.SelectDropTarget(hTarget);
        m_pDragImgList->DragShowNolock(true);
        m_hDragTarget = hTarget;
      }
    }

    // move image being dragged

    m_pDragImgList->DragMove(point);
  }

  CTreeView::OnMouseMove(nFlags, point);
}

The call to HitTest allows to know if the cursor is on a tree item, and which one ; if so, and if this item is different from the current target, it becomes the new target.

Note that SelectDropTarget is surrounded by 2 calls to DragShowNolock. When the target changes, the corresponding parts of the tree are redrawn to unselect the old target and highlight the new one. But a picture of the item being moved is displayed on top of the tree as a sprite, that is to say the background it covers is saved in order to be restored when it will move again. If the background changes without the picture knowing it, this can lead to this kind of undesirable "trails" :


So the rule is the following : every function call that modifies the display of the tree (like SelectDropTarget does) must be preceded by a DragShowNolock(false) which removes the picture of the item being moved, and in general followed by a DragShowNolock(true) unless you don't want to show the picture again.

Finally, the call to DragMove moves the picture of the dragged item according to the mouse position, be a new target selected or not.


Exit

The end of a drag & drop corresponds to the function below :

void CDragDropView::OnLButtonUp(UINT nFlags, CPoint point) 
{
  if(m_boDragging)
  {
    KillTimer(1);

    ReleaseCapture();
    ShowCursor(true);
    m_boDragging = false;

    m_pDragImgList->DragLeave(this);
    m_pDragImgList->EndDrag();
    delete m_pDragImgList;
    m_pDragImgList = NULL;

    GetTreeCtrl().SelectDropTarget(NULL);
    GetTreeCtrl().SelectItem(m_hDragItem);

    if(m_hDragTarget && (m_hDragTarget != m_hDragItem))
    {
      SuccessfulDrag(m_hDragTarget,m_hDragItem);
    }
  }   
  
  CTreeView::OnLButtonUp(nFlags, point);
}

It contains nothing extraordinary. Note the KillTimer call that cancels the SetTimer call in OnBeginDrag, and will be explained in the 'scrolling' paragraph. In the same way, ReleaseCapture stops the SetCapture from the initialization.


Result

The SuccessfulDrag function is not part of MFC : it's a method I added to the CDragDropView class, and that is called each time a drag & drop ends. In fact, when the user releases the mouse button, the MFC work is finished and it's your job to do what you want with the parameters you now have at your disposal : the handle of the dragged item, and the one of the chosen target.

This job at least involves really executing the asked move, that is adding the chosen item as well as its subtree as a child of the target, and deleting the original item (this also deletes its subtree) ; actually there is no "Move" function in CTreeCtrl. In the 2 samples, these operations are carried out by SuccessfulDrag, InsertItemAndSubtree, CopySubtree (which is recursive), and CopyItem (which copies an item with all its attributes - text, images, ItemData -, as well as its state - expanded, checked -). See the source code for more details.

You can of course execute other operations in response to a drag & drop, or add validation tests (any type of item can perhaps not become a child of any other type, and so on).


Cancellation

The user may want to cancel a running drag & drop, but if he releases the left mouse button then the move will be accepted. A good solution is to say that one can cancel a drag & drop by clicking the right button, and to handle the WM_RBUTTONDOWN event this way :

void CDragDropView::OnRButtonDown(UINT nFlags, CPoint point) 
{
  if(m_boDragging)
  {
    KillTimer(1);

    ReleaseCapture();
    ShowCursor(true);
    m_boDragging = false;

    m_pDragImgList->DragLeave(this);
    m_pDragImgList->EndDrag();
    delete m_pDragImgList;
    m_pDragImgList = NULL;

    GetTreeCtrl().SelectDropTarget(NULL);
    GetTreeCtrl().SelectItem(m_hDragItem);
    GetTreeCtrl().EnsureVisible(m_hDragItem);
  }   
  
  CTreeView::OnRButtonDown(nFlags, point);
}

This code is very similar to the one in OnLButtonUp, except that SuccessfulDrag is of course not called, and the EnsureVisible line ensures the item the user had started to drag is still visible on the screen (in case there was a scrolling of the tree, see further).


Expanding / collapsing items

A tree is not always entirely expanded, it is convenient to hide some branches when they're not useful. When you drag an item, you can on the contrary need to see the content of such branches, looking for the appropriate target. Some programs do it like this : if the mouse stays 2 or 3 seconds motionless on a collapsed (= preceded by the symbol "+") item, this one is going to be expanded automatically ; the opposite operation (= collapse an expanded item) is generally not available.


"meshes" and "characters" are expanded,
"blocks" and "buildings" are collapsed

This method can lead to some unwanted manipulations, I chose another one that is even simpler : during a drag & drop, the control key is used to alternatively expand or collapse the current target. This is very easy to implement :

- in response to a WM_KEYDOWN event, you have to check if it's the control key that is pressed :

void CDragDropView::OnKeydown(NMHDR* pNMHDR, LRESULT* pResult) 
{
  TV_KEYDOWN*  pTVKeyDown = (TV_KEYDOWN*)pNMHDR;
  WORD wVKey = pTVKeyDown->wVKey;

  if(wVKey == VK_CONTROL) DragToggleItem();
  
  *pResult = 0;
}

- the DragToggleItem function that executes the corresponding action is as follows :

void CDragDropView::DragToggleItem()
{
  if(m_boDragging) 
  {
    if(m_hDragTarget)
    {
      m_pDragImgList->DragShowNolock(false);
      GetTreeCtrl().Expand(m_hDragTarget,TVE_TOGGLE);
      GetTreeCtrl().RedrawWindow();
      m_pDragImgList->DragShowNolock(true);
    }
  }
}

NB : despite its name, the CTreeCtrl::Expand method is the one that also collapses tree items.


Scrolling

The item being moved and its target are not necessarily visible on the screen at the same time, even when some branches of the tree are collapsed, and particularly when the tree contains lots of elements. So being able to scroll the tree content during drag & drop is needed, but one can't click the vertical scrollbar without releasing the mouse button and ending the operation. There are 2 options :

- scroll the tree with the mouse wheel. The default implementation in CTreeView answers this purpose very well, except that it can produce the "trails" problem already mentioned (in the 'execution' paragraph). In order to remedy it, it's enough to handle the WM_MOUSEWHEEL event :

BOOL CDragDropView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) 
{
  if(m_pDragImgList) m_pDragImgList->DragShowNolock(false);
  BOOL Res = CTreeView::OnMouseWheel(nFlags, zDelta, pt);
  if(m_pDragImgList) m_pDragImgList->DragShowNolock(true);
  return Res;
}

- make the tree scroll when the dragged element is out of the limits of the view, for example upward when it's above the tree. The issue is that the content of the tree must continue to scroll as long as the item is above the tree, even if the mouse doesn't move anymore, that is even if CDragDropView doesn't receive Windows messages anymore. That's where the timer I talked about earlier comes into play : it allows to check on a regular basis the position of the cursor with respect to the tree, and to do the necessary scrollings (furthermore at a constant speed).

At the same time, it embeds the code that was situated in OnMouseMove. As we already saw, this timer is initialized in OnBeginDrag (start of the drag & drop) and destroyed in OnLButtonUp (end of the drag & drop) or OnRButtonDown (cancellation). Its code is a bit long because of the 4 possible scrolling directions, but not especially difficult to understand.

void CDragDropView::OnTimer(UINT nIDEvent)
{
  if(m_boDragging)
  {
    POINT point;
    GetCursorPos(&point);
    ScreenToClient(&point);

    // highlight target

    CTreeCtrl& Tree = GetTreeCtrl();
    TVHITTESTINFO tvHit;
    tvHit.pt = point;
    HTREEITEM hTarget = Tree.HitTest(&tvHit);

    if(hTarget)
    {
      if(hTarget != m_hDragTarget)
      {                                                     // this test avoids flickering
        m_pDragImgList->DragShowNolock(false);
        Tree.EnsureVisible(hTarget);
        Tree.SelectDropTarget(hTarget);
        m_pDragImgList->DragShowNolock(true);
        m_hDragTarget = hTarget;
      }
    }

    // scroll tree

    else
    {
      RECT rect;
      Tree.GetClientRect(&rect);

      int iMaxV = Tree.GetScrollLimit(SB_VERT);
      int iPosV = Tree.GetScrollPos  (SB_VERT);

      // up
      if((point.y < rect.top -10) && iPosV)
      {
        HTREEITEM hPrev = Tree.GetPrevVisibleItem(Tree.GetFirstVisibleItem());
        m_pDragImgList->DragShowNolock(false);
        Tree.EnsureVisible(hPrev);
        m_pDragImgList->DragShowNolock(true);
      }

      // down
      if((point.y > rect.bottom +10) && (iPosV != iMaxV))
      {
        UINT Nb = Tree.GetVisibleCount();
        if(Nb != -1)
        {
          HTREEITEM hNext = Tree.GetFirstVisibleItem();
          for(UINT i = 0; i < Nb; i++) hNext = Tree.GetNextVisibleItem(hNext);
          m_pDragImgList->DragShowNolock(false);
          Tree.EnsureVisible(hNext);
          m_pDragImgList->DragShowNolock(true);
        }
      }

      int iPosH = Tree.GetScrollPos  (SB_HORZ);
      int iMaxH = Tree.GetScrollLimit(SB_HORZ);

      // left
      if((point.x < rect.left) && iPosH)
      {
        m_pDragImgList->DragShowNolock(false);
        Tree.SendMessage(WM_HSCROLL,SB_LINELEFT);
        m_pDragImgList->DragShowNolock(true);
      }

      // right
      if((point.x > rect.right) && (iPosH != iMaxH))
      {
        m_pDragImgList->DragShowNolock(false);
        Tree.SendMessage(WM_HSCROLL,SB_LINERIGHT);
        m_pDragImgList->DragShowNolock(true);
      }
    }

    m_pDragImgList->DragMove(point);
  }
  
  CTreeView::OnTimer(nIDEvent);
}


CTreeCtrl based application

You can be brought to use a CTreeCtrl if your view does not only include a tree, but also other controls (buttons, etc). This version is located in the DragDropCtrl directory. This time the CDragDropView class is derived from CFormView, and the corresponding IDD_FORMVIEW dialog box contains an MFC tree control :

 
IDD_FORMVIEW and the main styles of its tree

The tree's styles are no longer defined in a function (PreCreateWindow) but in the resource editor, which is simpler.

Another difference : to prevent undesirable "trails" when the tree scrolls, we previously needed to handle in CDragDropView the WM_MOUSEWHEEL messages received by the tree ; now this is not this class that directly manages the tree, and it's at the control level that we have to modify the default behaviour of OnMouseWheel. That means it is necessary to create a class derived from CTreeCtrl, I named it CTreeCtrlDrag.

BOOL CTreeCtrlDrag::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) 
{
  if(m_pDragImgList) m_pDragImgList->DragShowNolock(false);
  BOOL Res = CTreeCtrl::OnMouseWheel(nFlags, zDelta, pt);
  if(m_pDragImgList) m_pDragImgList->DragShowNolock(true);
  return Res;
}

This has an additional advantage : because we have a class that manages the tree alone, some functions that were located in CDragDropView and were only affecting the tree can now be part of CTreeCtrlDrag. These are the ones that copy an element (CopyItem) or a whole branch (CopySubtree, InsertItemAndSubtree) in order to reflect the user's drag & drop on the screen. This code separation, absent from the other example, is of course more satisfying.

(short note : CTreeCtrlDrag owns a m_pDragImgList member variable that is used in OnMouseWheel above ; it is a copy of the address of the CImageList stored in CDragDropView. The view could actually use the variable of its tree whenever it needs it, it's up to you)

The remainder of the application doesn't require a lot of comments :
- the only new function is CDragDropView::OnSize, that resizes the tree control when the view's dimensions change
- the TVN_KEYDOWN and TVN_BEGINDRAG messages are not associated with their functions in the message map by the same macros as in the CTreeView sample, but it's the ClassWizard who deals with creating those lines for you anyway


Reference

To fill my gaps about drag & drop, I mostly consulted :
MSDN
codeguru


back to top