Introduction
This article results from my experience on handling the tap-and-hold operations on Windows CE 3.0 (PocketPC 2002) devices. Documentation on this issue is not always exact nor immediately available. To make matters worse, the support for tap-and-hold provided by MFC 3.0 is flawed.
Tap-and-hold
Broadly speaking, the tap-and-hold (TAH) gesture is used in Windows CE applications as a replacement for the mouse right-click. One can see this in all Microsot-provided applications as a means to bring up a context menu, making its use quite pervasive. The user receives visual feedback from the system when a tap-and-hold operation begins through a number of red circles showing up in a clockwise circular fashion around the place where the user tapped the screen.
Detection and Handling
Detection of a TAH is handled by the SHRecognizeGesture
API. It is generally used in the WM_LBUTTONDOWN
message handler in either direct mode or notification mode. In direct mode the function returns GN_CONTEXTMENU
if it detected a tap-and-hold command, or 0
otherwise. In notification mode, it will send a WM_NOTIFY
message with a GN_CONTEXTMENU
to the parent window.
Don't Trust MFC
MFC's handling is done in CWnd::OnLButtonDown
and, unfortunately, is flawed. If you look in wincore.cpp
you see how it is implemented:
BOOL CWnd::SHRecognizeGesture(CPoint point,
BOOL bSendNotification )
{
SHRGINFO shrgi = {0};
shrgi.cbSize = sizeof(SHRGINFO);
shrgi.hwndClient = m_hWnd;
shrgi.ptDown.x = point.x;
shrgi.ptDown.y = point.y;
shrgi.dwFlags = SHRG_RETURNCMD;
if(GN_CONTEXTMENU == ::SHRecognizeGesture(&shrgi))
{
if(bSendNotification)
{
shrgi.dwFlags = SHRG_NOTIFYPARENT;
::SHRecognizeGesture(&shrgi); }
return TRUE;
}
else
return FALSE;
}
void CWnd::OnLButtonDown (UINT nState, CPoint point)
{
if (!SHRecognizeGesture(point))
Default();
}
That's why all MFC windows respond to TAH commands twice, not once. Worse, it makes all of your windows respond to TAH. As a matter of fact, you may not want this to happen. The solution is quite simple: make sure your OnLButtonDown
does not call CWnd
's but Default()
instead:
void CMyWnd::OnLButtonDown(UINT nState, CPoint point)
{
Default();
}
Unfortunately, you will have to do this for all the windows that you don't want to handle the TAH command. There are two exceptions, though: CListCtrl
and CTreeCtrl
, but more on these later.
Generic Handling Example
Let's assume you want to handle TAH on your CWnd
-derived window class. One option is:
void CMyWnd::OnLButtonDown(UINT nFlags, CPoint point)
{
SHRGINFO shrgi = {0};
shrgi.cbSize = sizeof(SHRGINFO);
shrgi.hwndClient = m_hWnd;
shrgi.ptDown.x = point.x;
shrgi.ptDown.y = point.y;
shrgi.dwFlags = SHRG_RETURNCMD;
if(GN_CONTEXTMENU == ::SHRecognizeGesture(&shrgi))
ContextMenu(point);
else
Default();
}
This is the direct mode, where SHRecognizeGesture
returns a value that notifies if a TAH happened. Note that I'm using the API version of the method, not CWnd
's. If a TAH is detected, the ContextMenu
method is called. Here's how it might be implemented:
void CMyCwnd::ContextMenu(CPoint point)
{
CMenu mnuCtxt;
CMenu* pMenu;
CWnd* pWnd;
if(!m_nMenuID)
return;
if(mnuCtxt.LoadMenu(m_nMenuID))
{
pWnd = (m_pWndMenu ? m_pWndMenu : AfxGetMainWnd());
pMenu =
mnuCtxt.GetSubMenu(0);
if(pMenu)
{
ClientToScreen(&point);pMenu->TrackPopupMenu(TPM_LEFTALIGN,
point.x, point.y, pWnd);
}
}
}
In this class, m_nMenuID
is a UINT
that holds the context menu's ID. If NULL
, no menu is shown. The m_pWndMenu
member variable holds a pointer to the command-processing window. This is useful if you want to show TAH context menus on controls placed in dialogs. In these situations you cannot use AfxGetMainWnd()
because your menu commands might be either grayed, or worse, handled by another (hidden) window. So, m_pWndMenu
will have the containing CDialog
pointer.
The same code may be used in notification mode (see next section).
Note: The way OnLButtonDown
was implemented is not mandatory. In some situations you may have to always call Default()
, depending on the underlying window functionality.
CListCtrl and CTreeCtrl
These are two different beasts, because they do implement the correct TAH functionality at the system level. Unfortunately, MFC's encapsulation destroys it. So, unlike other windows, if you want these two to have a correct TAH behaviour, call Default()
on OnLButtonDown
. Intuitive, right? These controls' TAH implementation use the notification method, so there are a couple of other things to do. Here is a sample for a CListCtrl
-derived class:
BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl)
ON_WM_LBUTTONDOWN ()
ON_WM_LBUTTONUP ()
ON_NOTIFY_REFLECT (GN_CONTEXTMENU, OnListContextMenu)
.
.
.
END_MESSAGE_MAP()
void CMyListCtrl::OnListContextMenu(NMHDR* pNMHDR, LRESULT* pResult)
{
CMenu mnuCtxt;
CMenu* pMenu;
CWnd* pWnd;
NMRGINFO* pInfo;
if(!m_nLstMenu)
return;
if(mnuCtxt.LoadMenu(m_nLstMenu))
{
pWnd = (m_pWndMenu ? m_pWndMenu : AfxGetMainWnd());
pMenu = mnuCtxt.GetSubMenu(0);
if(pMenu)
{
UINT uFlags;
CPoint pt;
pInfo = (NMRGINFO*)pNMHDR;
pt = pInfo->ptAction;
ScreenToClient(&pt);
m_iItemOnMenu = HitTest(pt, &uFlags);
m_bTapAndHold = RUE;
pMenu->TrackPopupMenu(TPM_LEFTALIGN,
pInfo->ptAction.x,pInfo->ptAction.y, pWnd);
}
}
}
void CMyListCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
m_bTapAndHold = FALSE;
Default();
}
void CMyListCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
CListCtrl::OnLButtonUp(nFlags, point);
m_bTapAndHold = FALSE;
}
The concept is similar, with two exceptions. First, we store the TAH status in the boolean m_bTapAndHold
variable. Second, we store the clicked item on the integer m_iItemOnMenu
.
The first may be use when processing NM_CLICK
notifications. In these notifications it is impossible to know if the user simply clicked the control, or if she is issuing a TAH command. By testing the m_bTapAndHold
we can implement the same behaviour of the Contacts application, where a single click means edit, and a TAH means context menu.
The second variable may be used by the ON_UPDATE_COMMAND_UI
handles to determine what menu options are available. Remember that m_iItemOnMenu
will be -1
if the user tapped outside the items area.