Last Modification: December 11, 1999

Why don't I get a WM_CONTEXTMENU message when I right click in a Tree Control?

The tree control enters its own modal message loop when you click an item. This modal loop processes the WM_RBUTTONUP message itself and doesn't pass it to the default handler (which would then generate the WM_CONTEXTMENU message).

A quick and dirty solution is to handle the WM_RBUTTONDOWN message and not call the default handler. A better but more involved solution is to handle the WM_NOTIFY NM_RCLICK message instead. If you handle this and defer it to your WM_CONTEXTMENU message handler, you'll get correct mouse and keyboard (Shift+F10, or the context menu key on expanded keyboards) operation.

Here's an MFC example which illustrates setting a reasonable popup position for keyboard activation (lots of programs don't bother with this, with the result that keyboard activated popup menus don't appear where you'd expect). The example also illustrates a technique to simulate what Explorer does when you right click an item that's not currently selected.


/* This class member variable is used to allow the right
 * click operation to temporarily switch the selected item.
 * Make it a private class member variable of your derived
 * Tree control class
 */
HTREEITEM m_hTempSelect;

void CMyTreeView::OnRclick(NMHDR* /*pNMHDR*/, LRESULT* pResult)
{
	CTreeCtrl & tc = GetTreeCtrl();

	/* Get the mouse cursor position */
	DWORD dwPos = GetMessagePos();

	/* Convert the co-ords into a CPoint structure */
	CPoint pt( GET_X_LPARAM( dwPos ), GET_Y_LPARAM( dwPos ) ), spt;
	spt = pt;

	/* Convert to screen co-ords for hittesting */
	tc.ScreenToClient( &spt );

	UINT test;
	HTREEITEM hti = tc.HitTest( spt, &test );

	/* Did the click occur on an item */
	if ( ( hti != NULL ) && ( test & TVHT_ONITEM ) )
	{
		HTREEITEM htCur = tc.GetSelectedItem();

		/* Store the value of the right clicked item only *if*
		 * it's different from the currently selected item.
		 */
		if ( hti != htCur )
		{
			m_hTempSelect = hti;
		}

		/* Do the context menu */
		OnContextMenu( this, pt );
	}

	*pResult = 0;
}

void CMyTreeView::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
{
	CTreeCtrl & tc = GetTreeCtrl();
	CMenu menu;

	menu.LoadMenu( IDR_TREE_CONTEXT_MENU );

	CMenu * pop;

	pop = menu.GetSubMenu(0);

	/* If activated by the keyboard, get
	 * the position of the selected item
	 */
	if ( point.x == -1 )
	{
		HTREEITEM ht = tc.GetSelectedItem();

		RECT rect;

		tc.GetItemRect( ht, &rect, true );
		tc.ClientToScreen( &rect );

		/* Offset the popup menu origin so
		 * we can read some of the text
		 */
		point.x = rect.left + 15;
		point.y = rect.top + 8;
	}

	/* To ensure that commands and menu update handling is done
	 * properly, make the parent of the menu the main frame window
	 */
	CWnd * pMenuParent = AfxGetMainWnd();

	UINT uCmd = pop->TrackPopupMenu( TPM_RETURNCMD | TPM_LEFTALIGN |
				TPM_RIGHTBUTTON, point.x, point.y,
				pMenuParent, NULL );

	/* Menu item chosen ? */
	if ( uCmd != 0 )
	{
		/* Temporarily select any right-clicked item that's
		 * different from the currently selected item
		 */
		if ( m_hTempSelect )
		{
			HTREEITEM hOldSel = tc.GetSelectedItem();

			tc.Select( m_hTempSelect, TVGN_CARET );

			/* Execute the selected menu command */
			pMenuParent->SendMessage( WM_COMMAND, uCmd, 0 );

			tc.Select( hOldSel, TVGN_CARET );
		}
		else
		{
			/* Execute the selected menu command */
			pMenuParent->SendMessage( WM_COMMAND, uCmd, 0 );
		}
	}

	/* No longer need the temporary value */
	m_hTempSelect = NULL;

	menu.DestroyMenu();
}