You might call them tool tips, fly-by hints, balloon help, or something else. Whatever you call them, one of the most useful user interface additions of the last several years is the ability to display a small amount of explanatory text when the mouse hovers over each part of the user interface. Tool tip text can explain what a toolbar button does, explain what will happen when you press the OK button, and so on. That short text hint is often enough so that the user can proceed without looking something up in online help, or consulting a manual.
CToolBarCtrl is a good example of an attempt to automate the coding for tool tip display. If youre using third-party user interface code, however, you may face some extra work in supporting tool tips. At a minimum, you will have to study the third-party documentation or, ideally, hope to find a sample. Then you may have to carefully insert scattered processing in your code to trap messages, fill structures, relay events, etc. A software manufacturer may or may not provide tool tip support. But if it does, it probably has its own tool tip function, like that in Stingrays Objective Grid.
This is the situation I met recently while developing a slightly larger application, where we needed to put tool tips on a GPSs map view, on a Stingray Grid, and on some controls without tool tip support. So, I decided to create a generic CTipWnd class for all these uses. CTipWnd works in an intuitive way to show tips without you having to care about messages and structures. After attaching a CTipWnd object to its owner window, I just make one call to set a tip text wherever I want, even without passing coordinates. CTipWnd can set tip attributes such as font, color, and duration, and provides hit testing. You can pick it up and use it with just a few minutes work.
About Tool Tip Programming
A tool tip is a small popup window that usually displays a single line of text describing the purpose of a tool in an application. A tool tip appears when a user positions the mouse cursor on a tool and leaves it there for a certain interval. The tip disappears when a given duration expires or the user moves the cursor off the tool. In this context, a tool can be a window, a control, or a usually rectangular object within a windows client area. The tool tip is available under Windows 95, NT 3.51, and later.
MFC supports three classes for tool tip programming: CWnd, CToolBarCtrl, and CToolTipCtrl. To use tool tip support in CWnd, couple its member functions with your CWnd-derived class for enabling, message filtering, canceling, or hit testing. You must handle the TTN_NEEDTEXT notification with its TOOLTIPTEXT structure. But much worse, all these are not clearly documented, and youll need trial and error to get familiar with them. If youre not careful, these separated tooltip handlers can mess up your tidy object-oriented design. CToolTipCtrl does encapsulate the tool tip attributes and operations in one class, but its main use seems to lie in Windows common controls.
Third-party software classes like Stingrays CGXGridCore often provide their own version of tool tip support, with proprietary functions and definitions. Thats another reason programs can end up with similar, but not identical, tool tip code spread across multiple classes and modules. It would be helpful if a single generic tool tip class could handle all these cases.
In this article, Ill supply a class called CTipWnd that lets you add tool tips to your application easily. Figure 1 shows a sample using CTipWnd in different ways. I made four tips appear simultaneously, just for demonstration, as I can set a long display duration or even let a tip stay displayed after the cursor leaves the tool. The demonstration program shown in Figure 1 mainly demonstrates three separate features.
The first tip shown (This is the Second Demo Guide...) is related to a CTipWnd object bound to the dialog window and applies to any area where WM_MOUSEMOVE can be trapped. To display this tip, move the mouse over the top three static controls or the group box.
The second approach pertains to the other two tips, which are the tip shown coordinates (10, 20) and the tip below the Disable Date button. They represent the case where the parent window wont get the necessary information, because the child window is eating the WM_MOUSEMOVE messages. Some Windows controls and third party components work this way. Here, I illustrated this condition by using two derived classes from CListBox and CButton, each with an embedded CTipWnd member.
The tip shown with the date at the bottom is created by a CTipWnd object without a timer. You can press the Date button to enable or disable this tip, like turning on or off a message box. The similarity in all three approaches is that you just call CTipWnds member Create() at initialization and later call SetTipText() anywhere thats convenient.
tipwnd.h (Listing 1) and tipwnd.cpp (Listing 2) show how CTipWnd is implemented. Most of its members have self- explanatory names. The four tip attributes (font, background color, duration, and style) can be manipulated by the four corresponding set functions. The default attributes are defined in tipwnd.h. As for style, I define TWS_SHOWABOVE to show a tip above the cursor and TWS_ROURDRECT to draw a round rectangle; the default is a tip displayed below the cursor and in a normal rectangle. You could extend this code to add functions to inspect the given attributes, or to add more attributes and styles.
I decided to keep the tip window around for the lifetime of the corresponding CTipWnd object; I make the tip appear or disappear by showing or hiding the tip window. However, I only create the associated timer when the tip window is actually visible, which happens in CTipWnd::SetTipText(). I kill the timer when I hide the tip window, which can happen in OnTimer(), OnClose(), or CancelTipWnd(). m_nSecCount is a seconds counter and also acts as a flag that indicates whether the tip window is currently visible or hidden.
m_idTimer contains the timer ID. I chose to require the caller to pass in a timer ID when calling CTipWnd::Create(), in addition to the owner window pointer. This is extra work for the caller, but lets you use an existing timer if you have created one already. You would most likely create a single timer that gets used sequentially by any number of CTipWnd objects. This parameter defaults to zero, and in that case, I create a tool tip without a timer. Create() sets the default attributes and creates a tip window. I could let AfxRegisterWndClass() generate a tip window class name, but for simplicity, I use the predefined STATIC class.
CTipWnd offers three overloaded functions named SetTipText() for displaying tips. SetTipText(CString strTip, CPoint point) implements all the basic processing. It simply returns if the cursor position has not changed; note that you can get more than one WM_MOUSEMOVE message even if the mouse is not moving. If the CTipWnd object has an associated timer, this version of SetTipText() sets the timer in seconds and gets the next duration. It then calls Invalidate(), which in turn invokes OnPaint() to do all the drawing chores, and then moves the tip window to the desired position, on top and non-active, where m_size is the tip window size calculated in OnPaint().
The second version, SetTipText(CString strTip, CWnd* pWnd), is called to set tips without passing coordinates, since I can retrieve the cursor position from Win32s GetCursorPos(). The second parameter, pWnd, often points to a child window, and thus I combine the hit testing with a Boolean value returned. I make pWnd default to NULL to bypass the position checking if you dont need it.
The third version, SetTipText(CWnd* pWnd), can be called without supplying a tip text, and it uses pWnds window text instead. Table 1 summarizes the four different ways you might invoke SetTipText().
The source code to the demonstration program shown in Figure 1 is in tipdemo.h and tipdemo.cpp, included with this months code archive. As I mentioned before, in the main dialogs OnInitDialog(), I first create two CTipWnd objects by:
m_tipDlg.Create(this, 1); m_tipDate.Create(this);
where m_tipDlg with the timer ID 1 is for the static controls and the group box, and the non-timer m_tipDate is used to toggle the date tip. A small trick is that I call m_tipDate.SetTipText() in OnMove() instead of in OnDateBtn() directly so that I can make the date tip stick on the dialog when it is moved.
I derive the coordinate tracking window CPositionWnd from CListBox and dynamically subclass it ready for processing the WM_MOUSEMOVE message. This is a good example of how to use CTipWnd in such Windows controls or third party components. I let m_tipPos be the member of CPositionWnd, and in its constructor call:
m_tipPos.Create(this, 3); m_tipPos.SetTipStyle(TWS_SHOWABOVE); m_tipPos.SetTipDuration(1); m_tipPos.SetTipBkClr(RGB(255, 255, 0));
which gives m_tipPos the timer ID 3, duration one second, background dark yellow, and style of a tip above the cursor. In CPositionWnd::OnMouseMove(), I do the following:
if (m_tipPos.PtInOwnerWnd(point, 1)) m_tipPos.SetTipText(str, point); else m_tipPos.CancelTipWnd();
where point is the position passed by OnMouseMove(). You may wonder why I did hit testing here. Note that the owner of m_tipPos is the object of CPositionWnd, rather than the dialog parent, so that CPositionWnd::OnMouseMove() only can detect the mouse cursor in this tip owners window. Therefore, I leave a one-pixel margin around this window for border checking in PtInOwnerWnd().
Go back to tipwnd.cpp (Listing 2) and take a look at the two hit testing functions. PtInOwnerWnd(CPoint point, int iDeflate) checks whether a position is within the tips owner window. Its second parameter, iDeflate(default to zero) can be used to deflate the window rectangle a few pixels as I did above. Another function, PtInChildWnd(CWnd* pWnd, CPoint point, BOOL bClient) can be used by the parent window to check whether a cursor is in its child window pointed by the first parameter pWnd. As an example, I have called this function in SetTipText(CString strTip, CWnd* pWnd).
I have described the implementation of a generic tool tip class CTipWnd. You can easily decorate CTipWnd with more attributes or styles to suit your own purposes. More advanced tinkering could yield multiple-line text, hit testing for different shapes, processing mouse-click messages, and loading icons or bitmaps, depending on your requirements.
One point I havent spent much time on is how to determine the exact size of a cursor. Figure 2 shows a bitmap of arrow1.cur. The size of a cursor is usually 32 pixels by 32 pixels which is also the result from GetSystemMetrics() by SM_CXCURSOR and SM_CYCURSOR. In fact, the lower end of the cursor display does not always fit the bottom of its bitmap. This wasnt the main point of the article, so to keep things simple, I simply subtract 12 from the vertical size of the cursor if a tip should be below it; refer to the icyOffset assignment in SetTipText() in tipwnd.cpp (Listing 2).
Finally, as a bonus, CTipWnd can be easily adapted to a 16-bit version so that adding the tool tip support to a Win16 application becomes much simpler. Except for a few functions like CFont::CreatePointFont() and CRect::DeflateRect(), most others are compatible with the 16-bit version of VC++.
Zuoliu Ding works at Comarco Wireless Technologies in Irvine, CA, with field measurement software for communication networks. He can be reached at firstname.lastname@example.org.