Allowing multiple selection


The tree view control has very limited support for multiple selection. It provides a message to set and retrieve the selection state of an item. On the other hand it does not allow setting focus to a particular item without also selecting it. The side effect of this is that previous item is deselected. This causes a little bit of complication as you will see.

Before we go on I'd like to point to a couple of other sources available to help implement multiple selection. The October 1996 issue of MS Journal has an article by Paul DiLascia which gets you started on implementing multiple selection. Another source code availabe is from Bendik Engebretsen from his web site at http://www.techsoft.no/bendik/. He has provided a more full featured implementation and you may wish to study his code too before you implement this in your application.

Step 1: Add member variable to track first item in selection set

To emulate the shift selection in a listbox and a list view control, we need to track the first item in the selection set. This is useful when the user changes the selection set further. Add a protected member in your class declaration and initialize it to NULL in the constructor.
protected:
	HTREEITEM m_hItemFirstSel;		// Init to NULL in constructor

Step 2: Add code to WM_LBUTTONDOWN handler

We will add code to the WM_LBUTTONDOWN to handle multiple selection. There are a few things about the tree view control that you should be aware of. When you call the the SelectItem() or when the control selects an item in response to a mouse click, the previous item is deselected. For our purposes we often have to reselect the previous item. Using SetItemState() does not have this side effect. You may ask why not use SetItemState() instead of using SelectItem() or letting the control handle the mouse click. The answer is that SetItemState() does not set focus to the item whereas the other alternatives do. Unlike the list view control, the tree view control does not have the counterpart of LVIS_FOCUSED to set focus to an item.

The second issue is drag and drop. If the control does not receive the WM_LBUTTONDOWN message, it will not initiate a drag operation. So if you want to support control drag-drop or shift drag-drop you could either let the control participate in the WM_LBUTTONDOWN handling or you could initiate the drag-drop in your own WM_MOUSEMOVE handler. I chose to take the former approach.

The final issue is because of the decision to let WM_LBUTTONDOWN message pass on to the control. The built in handling causes a visible and momentary deselection of the previous item and if we use SetRedraw(FALSE), the control overrides it and causes the whole control to flicker. We could overcome this by calling the SetRedraw() function for the parent window but this can cause a redraw problem with the titletip. A titletip is the window that comes up when we move the cursor over an item that does not fit within the controls width. Its easier to let the momentary deselection remain.

Our decision to pass on the message causes yet another. Clicking on an item that is already selected initiates an edit. This one is easily solved, we deselect the item before passing the message on.

There are three different conditions we check for in this function. Let's cover them one by one. The first condition we check for is whether the control key is down. The usual behaviour of a control click is to toggle the selection state of the item. Before we try toggling the selection state we ascertain that the click is indeed on an item. We then determine the state of the item that will get the focus and the item that will lose the focus and set the appropriate state after calling the base class version of OnLButtonDown().

The shift-click always results in one block of adjoining items being selected. We need to track the starting item of a shift-selected block. This helps when there is further activity with shift-click or shift-arrow keys. The SelectItems() is a helper function that goes through all the items in the tree control and selects the items that belong within the selection block and deselects the rest.

Then there is the normal click with neither the control nor the shift key down. This is the simplest. We just clear any selection and let the default control behaviour do the job.

void CTreeCtrlX::OnLButtonDown(UINT nFlags, CPoint point) 
{
	// Set focus to control if key strokes are needed.
	// Focus is not automatically given to control on lbuttondown

	m_dwDragStart = GetTickCount();

	if(nFlags & MK_CONTROL ) 
	{
		// Control key is down
		UINT flag;
		HTREEITEM hItem = HitTest( point, &flag );
		if( hItem )
		{
			// Toggle selection state
			UINT uNewSelState = 
				GetItemState(hItem, TVIS_SELECTED) & TVIS_SELECTED ? 
							0 : TVIS_SELECTED;
            
			// Get old selected (focus) item and state
			HTREEITEM hItemOld = GetSelectedItem();
			UINT uOldSelState  = hItemOld ? 
					GetItemState(hItemOld, TVIS_SELECTED) : 0;
            
			// Select new item
			if( GetSelectedItem() == hItem )
				SelectItem( NULL );		// to prevent edit
			CTreeCtrl::OnLButtonDown(nFlags, point);

			// Set proper selection (highlight) state for new item
			SetItemState(hItem, uNewSelState,  TVIS_SELECTED);

			// Restore state of old selected item
			if (hItemOld && hItemOld != hItem)
				SetItemState(hItemOld, uOldSelState, TVIS_SELECTED);

			m_hItemFirstSel = NULL;

			return;
		}
	} 
	else if(nFlags & MK_SHIFT)
	{
		// Shift key is down
		UINT flag;
		HTREEITEM hItem = HitTest( point, &flag );

		// Initialize the reference item if this is the first shift selection
		if( !m_hItemFirstSel )
			m_hItemFirstSel = GetSelectedItem();

		// Select new item
		if( GetSelectedItem() == hItem )
			SelectItem( NULL );			// to prevent edit
		CTreeCtrl::OnLButtonDown(nFlags, point);

		if( m_hItemFirstSel )
		{
			SelectItems( m_hItemFirstSel, hItem );
			return;
		}
	}
	else
	{
		// Normal - remove all selection and let default 
		// handler do the rest
		ClearSelection();
		m_hItemFirstSel = NULL;
	}

   CTreeCtrl::OnLButtonDown(nFlags, point);
}

Step 3: Add code to the WM_KEYDOWN handler

The code in OnKeyDown will allow user to use the shift-up arrow and the shift-down arrow keys to create and modify the selection. If the key pressed is any other non control character, then the selection is cleared.
void CTreeCtrlX::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
	if ( (nChar==VK_UP || nChar==VK_DOWN) && GetKeyState( VK_SHIFT )&0x8000)
	{
		// Initialize the reference item if this is the first shift selection
		if( !m_hItemFirstSel )
		{
			m_hItemFirstSel = GetSelectedItem();
			ClearSelection();
		}

		// Find which item is currently selected
		HTREEITEM hItemPrevSel = GetSelectedItem();

		HTREEITEM hItemNext;
		if ( nChar==VK_UP )
			hItemNext = GetPrevVisibleItem( hItemPrevSel );
		else
			hItemNext = GetNextVisibleItem( hItemPrevSel );

		if ( hItemNext )
		{
			// Determine if we need to reselect previously selected item
			BOOL bReselect = 
				!( GetItemState( hItemNext, TVIS_SELECTED ) & TVIS_SELECTED );

			// Select the next item - this will also deselect the previous item
			SelectItem( hItemNext );

			// Reselect the previously selected item
			if ( bReselect )
				SetItemState( hItemPrevSel, TVIS_SELECTED, TVIS_SELECTED );
		}
		return;
	}
	else if( nChar >= VK_SPACE )
	{
		m_hItemFirstSel = NULL;
		ClearSelection();
	}
	CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}


Step 4: Add helper function to clear the selection

This function is called very often. Every time a user clicks on the tree view control without the shift or the control key pressed, this function gets called. The function as given below is very simple. It scans through all the items in the tree view control and deselects them individually. If the tree holds a lot of items, then this function can prove to be too slow. The GetNextItem() function used below is an overloaded function and has been defined in a previous section.

void CTreeCtrlX::ClearSelection()
{
	// This can be time consuming for very large trees 
	// and is called every time the user does a normal selection
	// If performance is an issue, it may be better to maintain 
	// a list of selected items
	for ( HTREEITEM hItem=GetRootItem(); hItem!=NULL; hItem=GetNextItem( hItem ) )
		if ( GetItemState( hItem, TVIS_SELECTED ) & TVIS_SELECTED )
			SetItemState( hItem, 0, TVIS_SELECTED );
}

Step 5: Add helper function to select a range of items

This helper function is used when the user shift-clicks on an item. It takes care of removing selection from items not within the range and selects the items inside the range.
// SelectItems	- Selects items from hItemFrom to hItemTo. Does not
//		- select child item if parent is collapsed. Removes
//		- selection from all other items
// hItemFrom	- item to start selecting from
// hItemTo	- item to end selection at.
BOOL CTreeCtrlX::SelectItems(HTREEITEM hItemFrom, HTREEITEM hItemTo)
{
	HTREEITEM hItem = GetRootItem();

	// Clear selection upto the first item
	while ( hItem && hItem!=hItemFrom && hItem!=hItemTo )
	{
		hItem = GetNextVisibleItem( hItem );
		SetItemState( hItem, 0, TVIS_SELECTED );
	}

	if ( !hItem )
		return FALSE;	// Item is not visible

	SelectItem( hItemTo );

	// Rearrange hItemFrom and hItemTo so that hItemFirst is at top
	if( hItem == hItemTo )
	{
		hItemTo = hItemFrom;
		hItemFrom = hItem;
	}


	// Go through remaining visible items
	BOOL bSelect = TRUE;
	while ( hItem )
	{
		// Select or remove selection depending on whether item
		// is still within the range.
		SetItemState( hItem, bSelect ? TVIS_SELECTED : 0, TVIS_SELECTED );

		// Do we need to start removing items from selection
		if( hItem == hItemTo ) 
			bSelect = FALSE;

		hItem = GetNextVisibleItem( hItem );
	}

	return TRUE;
}

Step 6: Add utility functions

Provide utility functions to query on the first selected item and to traverse the list of selected items in either the forward direction or the reverse direction. Again, like the ClearSelection() function, these functions can be time consuming for large trees.
HTREEITEM CTreeCtrlX::GetFirstSelectedItem()
{
	for ( HTREEITEM hItem = GetRootItem(); hItem!=NULL; hItem = GetNextItem( hItem ) )
		if ( GetItemState( hItem, TVIS_SELECTED ) & TVIS_SELECTED )
			return hItem;

	return NULL;
}

HTREEITEM CTreeCtrlX::GetNextSelectedItem( HTREEITEM hItem )
{
	for ( hItem = GetNextItem( hItem ); hItem!=NULL; hItem = GetNextItem( hItem ) )
		if ( GetItemState( hItem, TVIS_SELECTED ) & TVIS_SELECTED )
			return hItem;

	return NULL;
}

HTREEITEM CTreeCtrlX::GetPrevSelectedItem( HTREEITEM hItem )
{
	for ( hItem = GetPrevItem( hItem ); hItem!=NULL; hItem = GetPrevItem( hItem ) )
		if ( GetItemState( hItem, TVIS_SELECTED ) & TVIS_SELECTED )
			return hItem;

	return NULL;
}



Comments

  • There are no comments yet. Be the first to comment!

  • You must have javascript enabled in order to post comments.

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • This paper introduces IBM Java on the IBM PowerLinux 7R2 server and describes IBM's implementation of the Java platform, which includes IBM's Java Virtual Machine and development toolkit.

  • Agile methodologies give development and test teams the ability to build software at a faster rate than ever before. Combining DevOps with hybrid cloud architectures give teams not just the principles, but also the technology necessary to achieve their goals. By combining hybrid cloud and DevOps: IT departments maintain control, visibility, and security Dev/test teams remain agile and collaborative Organizational barriers are broken down Innovation and automation can thrive Download this white paper to …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds