I have come across many problems related to handling ToolTips in controls without parent involvement. Many samples show how to use ToolTips in a dialog by creating CToolTipCtrl, enabling it, and adding tools. Although it helps, it may turn out that achieving additional functionality proves to be difficult; for example: displaying different text for the same control or displaying information based on area in a window (rectangle). Implementing ToolTips proves to be more difficult for static controls created by inserting the control in a dialog template as well as those that are dynamically created. That is a main reason I have concentrated on the Static control.
All MFC CWnd-derived classes have automatic ToolTips built in. It is just a matter of enabling the tools and properly handling ToolTip messages. This approach may be used on any control, and any child window for that matter, for which we need to handle ToolTips independently of the parent window.
I have written a class that demonstrates ToolTip handling, derived from CStatic, called CStaticTTPSupport; the name is a little long, but serves the purpose. You should treat this class as a boilerplate and change code handling text assignment logic as well as determine a hit value, tailoring it to your needs.
A CWnd implementation of PreTranslateMessage filters messages that are needed for built-in ToolTips by calling FilterToolTipMessage. This in turn calls a virtual override of OnToolHitTest. A default implementation returns -1 if the window does not have children or if the window has ID_STATIC (-1). To change this behavior, the function must be overridden.
int CStaticTTPSupport::OnToolHitTest(CPoint point, TOOLINFO *pTI) const { pTI->hwnd = m_hWnd; pTI->uFlags = 0; // we need to differ tools // by ID, not window handle pTI->lpszText = LPSTR_TEXTCALLBACK; // tell ToolTips to send // TTN_NEEDTEXT for(int iIndx = 0; iIndx < _ZONE_COUNT * 2; iIndx++) { if(m_rectZone[iIndx].PtInRect(point)) { // point is in this rectangle. pTI->rect = m_rectZone[iIndx]; // TTN_NEEDTEXT Message will use this ID to fill NMHDR structure pTI->uId = iIndx; break; } } return iIndx; // return index of rectangle }
This function sets the TOOLINFO structure that is used by FilterToolTipMessage. The hwnd member is set to the control’s handle. It is important because this indicates the window that will receive TTN_NEEDTEXT notification because the lpszText member is initialized with LPSTR_TEXTCALLBACK. See code comments next to the member initialization.
In a nutshell: If the function returns value greater that -1, FilterToolTipMessage will determine the course of action that may show a new popup if needed and kill the old tool. If the hit’s value is -1, the ToolTip window is deactivated.
After setting tool information, you need a handler for TTN_NEEDTEXT notification.
- The notification message handler will always handle TTN_NEEDTEXT notification messages sent exclusively to a control; the control does not have any children. You do not need to indicate whether or not the message was handled; therefore, the ON_NOTIFY_EX macro is not needed.
- The ToolTip ID is always 0; therefore, the ON_NOTIFY_RANGE macro is not needed.
That is why I used the ON_NOTIFY macro to handle TTN_NEEDTEXT in this particular scenario. In other cases, it may be necessary using the macros that I mentioned above. Mapping macros look as follows:
ON_NOTIFY(TTN_NEEDTEXTW, 0, OnToolTipNotify) ON_NOTIFY(TTN_NEEDTEXTA, 0, OnToolTipNotify)
As you can see, I have mapped two versions of the TTN_NEEDTEXT notification code: ANSI and UNICODE. That may look as though it is unnecessary because the application that is built is not UNICODE. The real reason for handling both is not because the application may be built as UNICODE; it is a ToolTip control that, in newer versions of ComCtl32, have controls built for UNICODE and you may receive UNICODE. For the same reason, the TTN_NEEDTEXT handler handles ANSI and UNICODE versions.
In a sample class that you may have tuned to your needs, I have decided to assign different ToolTip text based on the cursor position over different areas of the window. The client area was divided into 16 rectangles in two rows and eight columns. ToolTip text is set depending on the cursor position over a given rectangle. This is determined in OnToolHitTest, where the hit value is assigned to a TOOLINFO structure.
The sample dialog uses three static controls: the first from a template, subclasses using class wizard; the second that is created and placed in dialog; and the third from a template subclassed without a wizard. The last one demonstrates how to subclass an already existing control by using its window handle, without regard to the control’s ID value (IDC_STATIC is used.)
Additional Information
TTN_NEEDTEXT notification was superseded with TTN_GETDISPINFO and TOOLTIPTEXT superseded with NMTTDISPINFO. For benefit of all programmers who use an older SDK and/or VS versions, I decided to use older definitions. This does not break anything because TTN_NEEDTEXT is defined as TTN_GETDISPINFO and TOOLTIPTEXT structure is defined as NMTTDISPINFO for newer releases.