Scrolling during drag and drop


When dragging an item it is quite possible that the drop target is not visible. Sometimes the user can collapse some items so that both the item to be dragged and the item that is to be the drop target are both visible. However, the user doesnt always have this recourse. Besides, most professional apps provide some form of scrolling while the item is being dragged.

In the implementation discussed below, we use a timer to enable us to drag the outline. We also scroll at varying speed depending on how far the cursor is from the border of the control.

Step 1: Add member variables to class

We declare two variables. One to hold the timer id and the other to hold a counter for the timer ticks. You may also choose to declare two more variable - for the event id that will be generated by the timer and the timeout value used for the timer. I have used hard coded values of 1 and 75 respectively.
protected:
	UINT    m_nTimerID;
	UINT    m_timerticks;

Step 2: Set timer in TVN_BEGINDRAG handler

We install the timer in the TVN_BEGINDRAG handler. Place the following statement at the end of you handler function ( I have used OnBeginDrag() ) in previous sections.
 
	// Set up the timer
	m_nTimerID = SetTimer(1, 75, NULL);

Step 3: Add a WM_TIMER handler

You can use the class wizard to add the WM_TIMER handler. By default this function is given the name OnTimer(). We first make sure that we are handling the right timer event. The next task is to update the drag image position.

We then determine whether scrolling is required. The criteria we use for this is that the cursor should be close to the top edge of the control or above the control. On the other end, if the cursor is near the bottom edge of the control or below the control, then we scroll in the other direction. We then compute a number to control the speed of scrolling. We have used a hard coded value of six different speed which should be enough. Each scroll speed range is 20 pixels tall. Ill leave the rest of the logic to control the scroll speed as an exercise for the reader - you.

You may notice that the call to DragMove() uses screen coordinates. The DragMove() function needs the screen coordinates if the call to DragEnter() when you began the drag operation had associtated the drag image to the desktop window. Use the client coordinates if the call to DragEnter() had specified the treeview control.

Before we go on the next step, Id like to discuss the behaviour of GetVisibleCount(). This function is supposed to return the number of visible items but instead it returns the maximum number of items that can be visible at one time. We therefore make a check for NULL before calling SelectDropTarget().
 

void CTreeCtrlX::OnTimer(UINT nIDEvent) 
{
	if( nIDEvent != m_nTimerID )
	{
		CTreeCtrl::OnTimer(nIDEvent);
		return;
	}

	// Doesn't matter that we didn't initialize m_timerticks
	m_timerticks++;

	POINT pt;
	GetCursorPos( &pt );
	RECT rect;
	GetClientRect( &rect );
	ClientToScreen( &rect );

	// NOTE: Screen coordinate is being used because the call
	// to DragEnter had used the Desktop window.
	CImageList::DragMove(pt);

	HTREEITEM hitem = GetFirstVisibleItem();

	if( pt.y < rect.top + 10 )
	{
		// We need to scroll up
		// Scroll slowly if cursor near the treeview control
		int slowscroll = 6 - (rect.top + 10 - pt.y) / 20;
		if( 0 == ( m_timerticks % (slowscroll > 0? slowscroll : 1) ) )
		{
			CImageList::DragShowNolock(FALSE);
			SendMessage( WM_VSCROLL, SB_LINEUP);
			SelectDropTarget(hitem);
			m_hitemDrop = hitem;
			CImageList::DragShowNolock(TRUE);
		}
	}
	else if( pt.y > rect.bottom - 10 )
	{
		// We need to scroll down
		// Scroll slowly if cursor near the treeview control
		int slowscroll = 6 - (pt.y - rect.bottom + 10 ) / 20;
		if( 0 == ( m_timerticks % (slowscroll > 0? slowscroll : 1) ) )
		{
			CImageList::DragShowNolock(FALSE);
			SendMessage( WM_VSCROLL, SB_LINEDOWN);
			int nCount = GetVisibleCount();
			for ( int i=0; i<nCount-1; ++i )
				hitem = GetNextVisibleItem(hitem);
			if( hitem )
				SelectDropTarget(hitem);
			m_hitemDrop = hitem;
			CImageList::DragShowNolock(TRUE);
		}
	}
}

Step 4: Kill the timer when dragging finishes

Call KillTimer( m_nTimerID ) whenever the drag operation finishes. This would include the PreTranslateMessage() if you are cancelling the drag on an escape key and OnLButtonUp().