A Generic Tool Tip Class



April 01, 2001
URL:http://www.drdobbs.com/a-generic-tool-tip-class/184416309

April 2001/A Generic Tool Tip Class


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 you’re 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 Stingray’s Objective Grid.

This is the situation I met recently while developing a slightly larger application, where we needed to put tool tips on a GPS’s 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 window’s 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 you’ll need trial and error to get familiar with them. If you’re 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 Stingray’s CGXGridCore often provide their own version of tool tip support, with proprietary functions and definitions. That’s 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.

My Solution

In this article, I’ll 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 won’t 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 CTipWnd’s member Create() at initialization and later call SetTipText() anywhere that’s convenient.

Implementing CTipWnd

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 Win32’s 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 don’t need it.

The third version, SetTipText(CWnd* pWnd), can be called without supplying a tip text, and it uses pWnd’s window text instead. Table 1 summarizes the four different ways you might invoke SetTipText().

Using CTipWnd

The source code to the demonstration program shown in Figure 1 is in tipdemo.h and tipdemo.cpp, included with this month’s code archive. As I mentioned before, in the main dialog’s 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 owner’s 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 tip’s 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).

Conclusion

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 haven’t 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 wasn’t 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 [email protected].

April 2001/A Generic Tool Tip Class/Figure 1

Figure 1: Demonstration of CTipWnd

April 2001/A Generic Tool Tip Class/Figure 2

Figure 2: Bitmap of arrow1.cur

April 2001/A Generic Tool Tip Class/Listing 1

Listing 1: tipwnd.h — Interface to CTipWnd

/*------------------------------------------------------------------
TipWnd.h:    Interface of the CTipWnd class
Author:      Zuoliu Ding, 11/2000
------------------------------------------------------------------*/
#ifndef _TIPWND_H__0A0771A8
#define _TIPWND_H__0A0771A8

#define DEF_COLOR        RGB(255, 255, 200)    // Light yellow
#define DEF_FONT         90, "Arial"           // Default font
#define DEF_DURATION     2                     // 2 Seconds
#define TWS_SHOWABOVE    0x00000001            // Tip above cursor
#define TWS_ROURDRECT    0x00000002            // Round tip rect 

class CTipWnd : public CWnd    
{
    UINT         m_idTimer;                    // Timer ID
    CWnd*        m_pOwner;                    // Parent window

    CString      m_strTip;                     // Tip text
    CPoint       m_point;                      // Coursor position
    CSize        m_size;                       // Tip size
    int          m_nSecCount;                  // Second count

    int          m_nSecDelay;                  // Duration 
    CFont        m_font;                       // Font
    CBrush       m_brush;                      // Back color
    DWORD        m_dwStyle;                    // Tip style

public:
    CTipWnd(): m_nSecCount(0), m_point(0,0), m_dwStyle(0) {}       
    ~CTipWnd() {}

    BOOL Create(CWnd* pOwner, UINT nTimerID=0);

// Operations
    void SetTipText(CString strTip, CPoint point);
    BOOL SetTipText(CString strTip, CWnd* pWnd=NULL);
    BOOL SetTipText(CWnd* pWnd);
    void CancelTipWnd();

    BOOL PtInOwnerWnd(CPoint point, int iDeflate=0);
    BOOL PtInChildWnd(CWnd* pWnd, CPoint point, BOOL bClient=TRUE);

// Attributes
    void SetTipFont(int nPtSize, LPCTSTR szFace);
    void SetTipBkClr(COLORREF clr);
    void SetTipDuration(int iSec) { m_nSecDelay =iSec; };
    void SetTipStyle(DWORD dwStyle) { m_dwStyle =dwStyle; };

protected:
    //{{AFX_MSG(CTipWnd)
    afx_msg void OnPaint();
    afx_msg void OnTimer(UINT nIDEvent);
    afx_msg void OnClose();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

#endif // _TIPWND_H__0A0771A8
//End of File
April 2001/A Generic Tool Tip Class/Listing 2

Listing 2: tipwnd.cpp — Implementation to CTipWnd

/*------------------------------------------------------------------
TipWnd.cpp:    Implementation of the CTipWnd class
Author:        Zuoliu Ding, 11/2000
------------------------------------------------------------------*/
#include "afxwin.h"
#include "TipWnd.h"

BOOL CTipWnd::Create(CWnd* pOwner, UINT nTimerID)
{
    m_idTimer   = nTimerID;
        m_nSecDelay = nTimerID ? DEF_DURATION: 0;
    m_pOwner    = pOwner;

    VERIFY(m_font.CreatePointFont(DEF_FONT));    // def font    
    m_brush.CreateSolidBrush(DEF_COLOR);         // def brush

    return CWnd::CreateEx(0, "STATIC", "", WS_POPUP,    
                            CRect(0, 0, 1, 1), pOwner, 0);
}

BEGIN_MESSAGE_MAP(CTipWnd, CWnd)
    //{{AFX_MSG_MAP(CTipWnd)
    ON_WM_PAINT()
    ON_WM_TIMER()
    ON_WM_CLOSE()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

// Message handlers
void CTipWnd::OnPaint() 
{
    CPaintDC dc(this); 

    CPen    penBlack;
    penBlack.CreatePen(PS_SOLID, 1, RGB(0, 0, 0));  
    CPen* pOldPen   = dc.SelectObject(&penBlack);
    CBrush* pOldBr  = dc.SelectObject(&m_brush);
    CFont* pOldFont = dc.SelectObject(&m_font);

    m_size = dc.GetTextExtent(m_strTip+"  "); 
    CRect rect  = CRect(0, 0, 0, 0);
    rect.right  = m_size.cx;
    rect.bottom = m_size.cy;

    if (m_dwStyle & TWS_ROURDRECT) dc.RoundRect(rect, CPoint(6, 6));
    else dc.Rectangle(rect);

    COLORREF oldClr = dc.SetTextColor(RGB(0, 0, 0));
    int nOldBkMode  = dc.SetBkMode(TRANSPARENT);
    dc.DrawText(m_strTip, -1, rect, 
                DT_SINGLELINE|DT_VCENTER|DT_CENTER);

    dc.SetTextColor(oldClr);
    dc.SetBkMode(nOldBkMode);
    dc.SelectObject(pOldFont);
    dc.SelectObject(pOldBr);
    dc.SelectObject(pOldPen);
}

void CTipWnd::OnTimer(UINT nIDEvent) 
{
    if (!m_hWnd || !m_idTimer) return; // No Wnd or timer

    if (--m_nSecCount ==0)
    {
        KillTimer(m_idTimer);
        ShowWindow(SW_HIDE);
    }
    CWnd::OnTimer(nIDEvent);
}

void CTipWnd::OnClose() 
{
    if (m_idTimer && m_nSecCount) KillTimer(m_idTimer); 

    m_font.DeleteObject();
    m_brush.DeleteObject();
    CWnd::OnClose();
}

// Operations
void CTipWnd::SetTipText(CString strTip, CPoint point)
{
    ASSERT(m_hWnd);
    if (m_idTimer && m_point==point) return;    
    m_point = point; 

    if (m_idTimer)                    // Need timer
    {
        if (!m_nSecCount)             // Tip window is hiden
            SetTimer(m_idTimer, 1000, NULL);
        m_nSecCount = m_nSecDelay;    // get another duration
    }

    m_strTip = strTip;
    ShowWindow(SW_SHOWNOACTIVATE); 
    Invalidate();
    UpdateWindow();

    int icyOffset = (m_dwStyle & TWS_SHOWABOVE)? 
                    -m_size.cy: GetSystemMetrics(SM_CYCURSOR)-12;
    m_pOwner->ClientToScreen(&point);
    SetWindowPos(&wndTop, point.x, point.y+icyOffset, 
                    m_size.cx, m_size.cy, SWP_NOACTIVATE);
}

BOOL CTipWnd::SetTipText(CString strTip, CWnd* pWnd)
{
    POINT pnt;        
    if (!GetCursorPos(&pnt)) return FALSE;
    if (pWnd && !PtInChildWnd(pWnd, pnt, FALSE)) return FALSE;

    m_pOwner->ScreenToClient(&pnt);
    SetTipText(strTip, pnt);
    return TRUE;
}

BOOL CTipWnd::SetTipText(CWnd* pWnd)
{
    if (!pWnd) return FALSE;
    CString str;
    pWnd->GetWindowText(str);
    if (str=="") return FALSE;

    return SetTipText(str, pWnd);
}

void CTipWnd::CancelTipWnd()
{
    if (!m_hWnd) return;

    if (m_idTimer) KillTimer(m_idTimer);
    ShowWindow(SW_HIDE);
    m_nSecCount =0;
}

BOOL CTipWnd::PtInOwnerWnd(CPoint pt, int iDeflate)
{
    CRect rc;
    m_pOwner->GetClientRect(rc);
    if (iDeflate) rc.DeflateRect(iDeflate,iDeflate);

    return rc.PtInRect(pt);
}

BOOL CTipWnd::PtInChildWnd(CWnd* pWnd, CPoint pt, BOOL bClient)
{
    CRect rc;
    pWnd->GetWindowRect(rc);
    if (bClient) m_pOwner->ClientToScreen(&pt);

    return rc.PtInRect(pt);
}

// Attributes
void CTipWnd::SetTipFont(int nPtSize, LPCTSTR szFace)
{
    m_font.DeleteObject();
    if (!m_font.CreatePointFont(nPtSize, szFace))
        m_font.CreatePointFont(DEF_FONT);       
}

void CTipWnd::SetTipBkClr(COLORREF clr)
{
    m_brush.DeleteObject();
    if (!m_brush.CreateSolidBrush(clr))    
        m_brush.CreateSolidBrush(DEF_COLOR); 
}

//End of File
April 2001/A Generic Tool Tip Class/Table 1

Table 1: How to call SetTipText()

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.