Tiptoe Through the ToolTips With Our All-Encompassing ToolTip Programmer's Guide

Roger Jack

Roger Jack is an independent consultant specializing in OOD, Windows GUI, and OLE. He can be reached via email at 73267.3161@compuserve.com.

ToolTips make using an application a lot easier. If you don't know what clicking on a toolbar button will do, just move your cursor over the toolbar button and wait for the ToolTip to appear. Although the status bar can provide more information than a ToolTip, your eyes do not have to travel to the bottom of the window to see a ToolTip.

Other types of tips have started to appear in other situations as well. There are TitleTips to extend the title of tree or listbox controls, DataTips to provide details about data in the window, and ToolTips for Web pages. In this article, I'll provide a comprehensive primer on adding tips to your application, from adding simple MFC-supplied ToolTips to writing custom TitleTips. Along the way, I'll even give you some pointers on adding ToolTips to your Web pages, including a simple ActiveX™ button control that has ToolTips. But before I begin discussing the specifics, let's briefly review MFC class support for ToolTips.

MFC Class Support for Tips

MFC has two classes that support ToolTips, CToolTipCtrl and CWnd. CToolTipCtrl encapsulates the functionality of the standard ToolTip control (found in the Common Controls DLL) and, therefore, can be used to create and manage a ToolTip control directly. In general, a single ToolTip can support multiple tools, which are just particular rectangles in a window and may or may not be child windows. It is also possible to have a single tool that fills the entire window. Information concerning a tool is sometimes passed through a TOOLINFO structure, which contains various fields: a handle to the window that contains the tool, an ID or window handle for the tool, a rectangle for the position of the tool, and information concerning the text for the tool. One of the most important methods is CToolTipCtrl::RelayEvent, which is used to relay mouse events to the ToolTip for processing. It makes sense to send mouse events to the ToolTip because it needs to determine when to display or hide a ToolTip. Unfortunately, CToolTipCtrl does not encapsulate all of the functionality of the standard ToolTip. For instance, CToolTipCtrl::SetDelayTime does not support all of the possible delay times. I sometimes had to use the raw Windows® messages and notifications because of the lack of total encapsulation. All ToolTip messages begin with the prefix "TTM_", and all ToolTip notifications begin with the prefix "TTN_". I will use this class extensively later, so I won't go into anymore detail here.

Microsoft recently enhanced the DLL that contains the ToolTips control (comctl32.dll) with Microsoft® Internet Explorer 4.0 (IE 4.0). The two-part MSJ article, "Previewing the Common Controls DLL for Microsoft Internet Explorer," (beginning in October, 1996) does an excellent job describing the new features. The new features include owner-draw, multiline, custom color ToolTips, and support for ToolTips that can track along with the mouse. There is a TTM_GETDELAYTIME message to get various delay times and a TTM_POP message to hide the ToolTip. Unfortunately, as I write this article, Microsoft has not yet added support for the new features in CToolTipCtrl. Therefore, I have to use CWnd::SendMessage to implement the functionality I'm showing you here. (The upcoming Visual Studio should have the long-awaited commctl.h file with the updated definitions—Ed.)

The CWnd class provides basic support for adding ToolTips to a window. Figure 1 shows the CWnd member functions that provide ToolTip support. CWnd::EnableToolTips enables or disables ToolTips for a window and should be called before calling any of the other methods. Note, however, that there is a flaw in CWnd::EnableToolTips: when you call CWnd::EnableToolTips with FALSE, it eventually calls another method that sends a message to deactivate the ToolTip. When you call CWnd::EnableToolTips with TRUE, it never reactivates the ToolTip.

Figure 1 CWnd Support for ToolTips

Member

Description

BOOL EnableToolTips(BOOL bEnable)

Enables or disables ToolTips for a window

virtual int CWnd::OnToolHitTest(CPoint point, TOOLINFO* pTI ) const

Called by the framework to determine if the cursor is over a tool that has a ToolTip

void FilterToolTipMessage(MSG* pMsg)

Checks to see if a message is relevant for displaying ToolTips

static void PASCAL CancelToolTips(BOOL bKeys)

Hides a ToolTip if one is currently displayed


CWnd::OnToolHitTest is called by the framework and you can override it to hit-test the tool area. The first argument, point, is the location of the cursor in client coordinates. Use this to compare the cursor to the known positions of your tools (or buttons). The second parameter is the TOOLINFO structure I described previously. I'll show you how to override CWnd::OnToolHitTest later.

CWnd::FilterToolTipMessage is normally called for you by CWnd::PreTranslateMessage. You may call CWnd::FilterToolTipMessage directly (typically from your own PreTranslateMessage function) if CWnd::PreTranslateMessage is not being called for you, and I'll show you how later. CWnd::CancelToolTips hides a displayed ToolTip. The bKeys parameter is set to TRUE to hide the ToolTip when a key is pressed. It is important to understand that, even though CWnd::CancelToolTips is a static member function, it only affects ToolTips managed by CWnd. In other words, it has no affect on a CToolTipCtrl that you create in your own code.

Behind the scenes, CWnd implements its ToolTip functionality by creating a CToolTipCtrl and managing it for you. It stores a pointer to the ToolTip as an m_pToolTip member variable of a hidden AFX_THREAD_STATE structure. This structure is used by MFC to store thread-specific information. CWnd provides no documented support for accessing this ToolTip directly.

Adding Simple ToolTips with MFC

Microsoft has made it simple to add ToolTips to toolbar buttons. In fact, if you use AppWizard, it is virtually automatic. To add ToolTips, make certain that you check the "Docking toolbar" checkbox when you generate your application with AppWizard. This causes AppWizard to insert an m_wndToolBar into CMainFrame. m_wndToolBar is a CToolBar that gets initialized in CMainFrame::OnCreate. The CToolBar class has built-in support for ToolTips. AppWizard adds strings to the resource file that CToolBar uses as ToolTips for the standard toolbar buttons.

It is easy to modify the ToolTip strings after you have generated the application. Open the toolbar in the resource view. Double-click any toolbar button to display the Toolbar Button Properties dialog for that button, then edit the prompt line after the \n to change the ToolTip text. For example, in Figure 2, editing "Open" after the \n modifies the ToolTip text. The string before the \n is the text that appears in the status line when the ToolTip is displayed.

Figure 2 Toolbar properties

As I mentioned, the ToolTip text is stored in the string table. The string ID of the ToolTip text is equal to the ID of the ToolBar button. In Figure 2, the string ID would be ID_ FILE_OPEN. Microsoft's support for toolbar ToolTips is so easy to use that not much can go wrong. The only problem I have ever encountered has been the result of somebody accidentally overwriting the strings in the string table.

Adding ToolTips to Modal Dialog Boxes

You have probably seen dialogs that have ToolTips for each individual control. This is very useful if the purpose of the control is not immediately obvious. Knowledge Base article Q141758 describes how to implement ToolTips in MFC dialogs, so I will only briefly describe the steps here. For MFC version 4.0 or greater, you need to perform the following steps, assuming that the dialog box already exists in your application:

For versions of MFC earlier than 4.0, CDialog::PreTranslateMessage is not automatically called by CDialog::DoModal, so you have to do some additional work to relay mouse events to the ToolTip control. More specifically, you must override CWinApp::ProcessMessageFilter to relay events to the ToolTip. CWinApp::ProcessMessageFilter is called by the MFC framework's hook function to react to certain Windows messages. See the Knowledge Base article for more details, including specific coding examples.

Adding ToolTips to Your Web Page

Like applications, Web pages can benefit from ToolTips. There are two obvious ways to use ToolTips on Web pages: ToolTips for graphics and ToolTips for ActiveX controls. The sample control for this article is a button that I wrote to demonstrate how simple it is to add ToolTips to ActiveX controls. Figure 3 shows both the ActiveX control and a graphic image. The ActiveX control is the button with the smiley face. The graphic is the little bit of "art" just below the button.

Figure 3 Web page ToolTip

Adding a ToolTip to a graphic image is simple because this functionality is built into HTML (see Figure 4). The line

<img src="Image.gif" height=48 width=48 
alt="Image ToolTip" >

provides the name and size of the image. The portion that says "Image ToolTip" is the actual ToolTip text that appears when you place the cursor over the image. It is also possible to add HotSpots to the image and create multiple ToolTips, but that is really beyond the scope of this article. I just want to give you a feel for how easy it is to add ToolTips via HTML.

Figure 4 HTML for a ToolTip

<OBJECT ID="WebButton1" WIDTH=31 HEIGHT=28
 CLASSID="CLSID:381C5023-2FDA-11D0-8BC1-444553540000">
    <PARAM NAME="_Version" VALUE="65536">
    <PARAM NAME="_ExtentX" VALUE="786">
    <PARAM NAME="_ExtentY" VALUE="731">
    <PARAM NAME="_StockProps" VALUE="0">
    <PARAM NAME="ToolTipText" VALUE="WebButton ToolTip Test">
</OBJECT>
</P>

<br><img  src="Image.gif"  height=48 width=48 alt="Image ToolTip" >

<SCRIPT LANGUAGE="VBScript">
<!--
Sub WebButton1_Click()
MsgBox "WebButton was clicked"
end sub
-->
    </SCRIPT>

The ActiveX control is a button with a ToolTip. Why would I bother making a button when I can add a three-dimensional image to the Web page that looks like a button anyway? Well, there are two reasons. First, a button is more realistic—it moves in and out the way a real button should when the user clicks on it. Second, I wanted to show you how to add ToolTips to ActiveX controls, and a button is about the simplest control that I could use.

I used AppWizard to initially generate the control. I turned on the "Activate when visible" option and turned all other options off. For the "Which window class, if any, should this control subclass?" option, I selected BUTTON. There is a significant amount of boilerplate code generated by AppWizard that is superfluous to the main point of this article. I will focus primarily on the code that I added, which is all contained in CWebButtonCtrl (see Figure 5 for a partial listing). Let's look at a couple of data members first. CWebButtonCtrl::m_bToolTipEnabled is TRUE if ToolTips are enabled. CWebButtonCtrl::m_strToolTipText contains the text for the ToolTip. I added both of these variables with ClassWizard and they represent OLE properties that are updated automatically by the MFC framework when the property is changed.

Figure 5 CWebButtonCtrl

// WebButtonCtl.cpp : Implementation of the CWebButtonCtrl OLE control class.

·

·

·

/////////////////////////////////////////////////////////////////////////////
// CWebButtonCtrl::RelayToolTipEvent - Pass mouse messages to ToolTip

void CWebButtonCtrl::RelayToolTipEvent(const MSG* pMsg)
{
    MSG MsgCopy;
    ::memcpy(&MsgCopy, pMsg, sizeof(MSG));
    FilterToolTipMessage(&MsgCopy);
}


int CWebButtonCtrl::OnToolHitTest(CPoint point, TOOLINFO* pTI) const
{
    if (m_bToolTipEnabled && pTI != NULL && pTI->cbSize >= sizeof(TOOLINFO))
    {
        // setup the TOOLINFO structure
        pTI->hwnd = m_hWnd;
        pTI->uId = 0;
        pTI->uFlags = 0;
        GetClientRect(&(pTI->rect));
        pTI->lpszText = LPSTR_TEXTCALLBACK;
    }

    return (m_bToolTipEnabled ? 1 : -1);
}


/////////////////////////////////////////////////////////////////////////////
// CWebButtonCtrl message handlers

int CWebButtonCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (COleControl::OnCreate(lpCreateStruct) == -1)
    {
        return -1;
    }
    
    if (m_Bitmap.LoadBitmap(IDB_WEBBUTTON))
    {
        SendMessage(BM_SETIMAGE, IMAGE_BITMAP, 
            (LPARAM)m_Bitmap.GetSafeHandle());
    }
    else
    {
        TRACE("Unable to load bitmap for button.");
    }

    EnableToolTips(TRUE);
    return 0;
}

void CWebButtonCtrl::OnMouseMove(UINT nFlags, CPoint point) 
{
    RelayToolTipEvent(GetCurrentMessage());
    COleControl::OnMouseMove(nFlags, point);
}

void CWebButtonCtrl::OnLButtonDown(UINT nFlags, CPoint point) 
{
    RelayToolTipEvent(GetCurrentMessage());
    COleControl::OnLButtonDown(nFlags, point);
}

void CWebButtonCtrl::OnLButtonUp(UINT nFlags, CPoint point) 
{
    RelayToolTipEvent(GetCurrentMessage());
    COleControl::OnLButtonUp(nFlags, point);
}

BOOL CWebButtonCtrl::OnToolNeedText(UINT id, NMHDR * pNMHDR, LRESULT * pResult)
{
    TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR;
    ::strcpy(pTTT->szText, m_strToolTipText);
    return TRUE;
}


 
/////////////////////////////////////////////////////////////////////////////
// Property changed handlers

void CWebButtonCtrl::OnToolTipEnabledChanged() 
{
    SetModifiedFlag();
}

void CWebButtonCtrl::OnToolTipTextChanged() 
{
    SetModifiedFlag();
}

Moving on to the methods, CWebButtonCtrl::PreCreateWindow manipulates the CREATESTRUCT structure passed to it. Notice that I made the button owner-draw by applying the BS_OWNERDRAW style bit. I needed to make the button owner-draw so I could avoid drawing the focus rect when the button is active. Otherwise, the focus rect would always be visible. As a side effect, this also means that I have to override CWebButtonCtrl::OnOcmDrawItem to handle all drawing for the button. CWebButtonCtrl::OnCreate loads a bitmap for the button by calling BM_SETIMAGE. It also calls CWebButton::EnableToolTips to take advantage of the CWnd support for ToolTips.

The CWebButtonCtrl::OnMouseMove, CWebButtonCtrl::OnLButtonDown, and CWebButtonCtrl::OnLButtonUp methods all work similarly; they all call CWnd::RelayToolTipEvent. The CWebButtonCtrl::RelayToolTipEvent method makes a non-const copy of the passed message and calls CWnd::FilterToolTipMessage. I made a copy of the passed message because CWnd::FilterToolTipMessage requires a non-const pointer to the message. I could have cast the pointer to a non-const, but this is dangerous since CWnd::FilterToolTipMessage could then modify the message. Normally, CWnd calls CWnd::FilterToolTipMessage automatically in CWnd::PreTranslateMessage. However, in an ActiveX control the mouse messages never get to CWnd::PreTranslateMessage because it is only called as a result of keyboard input. CWnd::PreTranslateMessage is used primarily to handle keyboard accelerators. In a regular MFC application, CWnd::PreTranslateMessage is called as a result of CWinThread::PumpMessage.

CWebButtonCtrl::OnToolHitTest is called by CWnd::FilterToolTipMessage, and I overrode the default implementation to properly fill in the passed TOOLINFO structure. It only fills in TOOLINFO if ToolTips are enabled for the control. A ToolTip will only be displayed if TOOLINFO is filled in. The other tests for NULL and size are sanity checks. When it does fill in TOOLINFO, the function sets the rect member of TOOLINFO to the size of the client rect. In other words, this makes the entire button a single tool. It sets the lpszText member of TOOLINFO to the LPSTR_CALLBACK. The ToolTip then sends the TTN_NEEDTEXT notification to get the text for the ToolTip. CWebButtonCtrl::OnToolNeedText handles the TTN_NEEDTEXT notification message from the ToolTip control by copying the string in m_strToolTipText into the szText data member of the passed TOOLTIPTEXT structure.

As you can see, the implementation of the ActiveX control relies on the CWnd support for ToolTips. Knowledge Base article Q141871 describes another method for adding ToolTips to ActiveX Controls: it shows how to create a CToolTipCtrl and call AddTool and UpdateTipText. A version of an ActiveX control using that technique is included with the downloadable source code (see page 5 for details). The amount of code required for both techniques is about the same in this example. Since I use the latter technique in the next tip, I want to expose you to CWnd support for ToolTips here.

I want you to be aware of the limitations of my implementation. First, there is a problem if you resize the control. The size of the tool does not get adjusted. This is not a real-world problem because you usually don't resize a button on a Web page after the Web page has been loaded. Second, a real-world ActiveX control would require code signing to create an authentication certificate. Otherwise, any user of the Web page will get a warning screen. This sample control is not signed. Third, the image is hardcoded into the button. A real-world ActiveX button should have the ability to load images dynamically.

I used the ActiveX Control Pad, which is available for free from Microsoft at http://www.microsoft.com/workshop/author/cpad/cpad.htm, to add the control to my Web page. Figure 4 shows the HTML code that was inserted. As you can see, the HTML includes values for OBJECT ID, WIDTH, HEIGHT, and CLASSID. There is also a list of parameters or properties for the control. The parameter name ToolTipText (with a value of "WebButton ToolTip Test") is the actual ToolTip text for the button. The line

<SCRIPT LANGUATE="VBScript">

is the beginning of a short VBScript routine that I wrote to respond to a button click. When you click the button, a "WebButton was clicked" message box appears.

Adding DataTips

DataTips are used to provide more detailed information about data that is displayed in a window. For instance, Microsoft uses DataTips in Visual C++® to make it easy to see variable contents. You just place your cursor on a variable when you are debugging and a tip pops up to display the contents of the variable. DataTips are useful in any situation in which more detail is available for an object in a window than can reasonably fit in the window.

In this example, I use the new ToolTip features made available with the IE 4.0 Common Controls DLL to create DataTips that contain information about displayed circles (see Figure 6). I create circles of various sizes and colors in random locations in the window. When the cursor is over a circle, a multiline DataTip appears that shows the position, radius, and color of the circle. The color of the DataTip text is the same color as the circle. The multiline and text color features are only available with IE 4.0 installed.

Figure 6 DataTip sample

I used AppWizard to generate a single document application with print preview turned off. I left all other settings set to their defaults. I created or modified three classes to implement the DataTip demo: CCircle, CDTDocument, and CDTView.

CCircle is a simple class that handles the drawing and hit-testing of a circle (see Figure 7 for the hit-testing code). CCircle::m_CenterPoint, CCircle::m_nRadius, and CCircle::m_Color hold the center, radius, and color of the circle, respectively. CCircle::Initialize takes a centerpoint, radius, and color as arguments and uses them to initialize the corresponding data members of a circle. I chose to have CCircle::Initialize initialize the data members rather than pass arguments in the constructor because it's easier to create an array of circles in CDTDocument. You'll see what I mean when I discuss CDTDocument below.

Figure 7 CCircle Hit-testing

/////////////////////////////////////////////////////////////////////////////
// CCircle hittesting

inline double Square(int n) { return (double(n) * double(n)); }

BOOL CCircle::HitTest(const CPoint& Point) const
{
    CPoint Diff = m_CenterPoint - Point;
    return ((Square(Diff.x) + Square(Diff.y)) <= Square(m_nRadius));
}

CCircle::Draw takes a pointer to a device context where the circle should paint itself. It calculates the bounding rectangle of the circle, creates an appropriately colored brush, and then uses CDC::Ellipse to draw the circle into the device context. CCircle::HitTest takes a point to be tested and uses the Pythagorean theorem (a2 + b2 = c2) to determine if the point is actually in the circle by checking if c2 is less than the circle's radius. I created an inline Square function to make the CCircle::HitTest code more readable. CCircle::GetColor, CCircle::GetCenter, and CCircle::GetRadius are inline methods that return the data members m_Color, m_CenterPoint, and m_nRadius, respectively. I made these member functions and the CCircle::Draw and CCircle::HitTest member functions const because they do not affect the internal state of the class. In other words, they maintain the constness of the class. This is good programming practice because it lets you use constant instantiations of CCircle.

CDTDemoDoc is derived from CDocument (which is included in the source code for this article—see page 5 for details on downloading the source code). It holds an array of CCircles and provides CDTDemoDoc::GetCircleCount and CDTDemoDoc::GetCircle to access information concerning the array. CDTDemoDoc::GetCircleCount takes a zero-based index that represents an offset into the array. CDTDemoDoc::m_CircleArray holds the CCircles and has a size of CIRCLECOUNT. I could have made this array public, but I didn't do that for two reasons. First, it's easier for me to change the implementation if I hide it. I might, for instance, want to use the CArray template in the future to create a variable-sized array. Second, I want to return const references to users of CDTDemoDoc::GetCircle. This makes it impossible for the user of CDTDemoDoc to accidentally modify the circles in the array. CDTDemoDoc::CDTDemoDoc calls CCircle::Initialize for each circle in the array. CCircle::Initialize makes it easy to create a fixed-size array because I don't have to pass parameters to the constructor of CCircle. Otherwise, I would have had to create the array dynamically. I used the rand function to randomize the position of the circles. Notice that by seeding the srand function with the current time, there is a high probability that the position of each circle will be different each time that you run the application.

CDTDemoView is responsible for displaying the circles and managing the DataTip (see Figure 8). CDTDemoView::m_ ToolTip holds the ToolTip that is used as a DataTip. I wanted this example to show you how to use the CToolTipCtrl class directly rather than using the CWnd support for ToolTips. However, even if I wanted to use the CWnd support, I couldn't because I need direct access to the ToolTip so that I can send messages to it. CWnd does not provide any documented way to access the ToolTip that it creates, and I wouldn't want to rely on implementation details. CDTDemoView::m_pCircleHit is the current circle hit by the mouse. CDTDemoView::m_pCircleHit can be NULL if no circle is currently hit.

Figure 8 DTDemoView

/////////////////////////////////////////////////////////////////////////////
// DTDemoView.cpp : implementation of the CDTDemoView class
·
·
·
/////////////////////////////////////////////////////////////////////////////
// CDTDemoView HitTest

const CCircle* CDTDemoView::HitTest(const CPoint& Point)
{
    CDTDemoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    // Check in reverse order to deal with clipping.
    const CCircle *pCircleHit = NULL;
    for (int n = pDoc->GetCircleCount() - 1; n >= 0 && pCircleHit == NULL; n--)
    {
        if (pDoc->GetCircle(n).HitTest(Point))
        {
            pCircleHit = &(pDoc->GetCircle(n));
        }
    }
    return pCircleHit;
}

/////////////////////////////////////////////////////////////////////////////
// CDTDemoView drawing

void CDTDemoView::OnDraw(CDC* pDC)
{
    CDTDemoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    for (int n = 0; n < pDoc->GetCircleCount(); n++)
    {
        pDoc->GetCircle(n).Draw(pDC);
    }
}

/////////////////////////////////////////////////////////////////////////////
// CDTDemoView diagnostics

#ifdef _DEBUG
void CDTDemoView::AssertValid() const
{
    CView::AssertValid();
}
void CDTDemoView::Dump(CDumpContext& dc) const
{
    CView::Dump(dc);
}

CDTDemoDoc* CDTDemoView::GetDocument() // non-debug version is inline
{
    ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CDTDemoDoc)));
    return (CDTDemoDoc*)m_pDocument;
}
#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////
// CDTDemoView message handlers

void CDTDemoView::OnInitialUpdate() 
{
    CView::OnInitialUpdate();
    CRect ClientRect(0, 0, 1000, 1000);
    if (m_ToolTip.Create(this, TTS_ALWAYSTIP) && m_ToolTip.AddTool(this))
    {
        m_ToolTip.SendMessage(TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
        m_ToolTip.SendMessage(TTM_SETDELAYTIME, TTDT_AUTOPOP, SHRT_MAX);
        m_ToolTip.SendMessage(TTM_SETDELAYTIME, TTDT_INITIAL, 200);
        m_ToolTip.SendMessage(TTM_SETDELAYTIME, TTDT_RESHOW, 200);
    }
    else
    {
        TRACE("Error in creating ToolTip");
    }
}

BOOL CDTDemoView::OnToolTipNeedText(UINT id, NMHDR * pNMHDR, LRESULT * pResult)
{
    BOOL bHandledNotify = FALSE;

    CPoint CursorPos;
    VERIFY(::GetCursorPos(&CursorPos));
    ScreenToClient(&CursorPos);

    CRect ClientRect;
    GetClientRect(ClientRect);

    // Make certain that the cursor is in the client rect, because the
    // mainframe also wants these messages to provide tooltips for the
    // toolbar.
    if (ClientRect.PtInRect(CursorPos))
    {
        TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR;
        m_pCircleHit = HitTest(CursorPos);

        if (m_pCircleHit)
        {
            // Adjust the text by filling in TOOLTIPTEXT
            CString strTip;
            const CPoint& Center = m_pCircleHit->GetCenter();
            COLORREF Color = m_pCircleHit->GetColor();
            strTip.Format("Center:  (%d, %d)\nRadius:  %d\nColor:  (%d, %d, %d)", 
                          Center.x, Center.y, m_pCircleHit->GetRadius(), 
                          (int)GetRValue(Color), (int)GetGValue(Color),  
                          (int)GetBValue(Color));
            ASSERT(strTip.GetLength() < sizeof(pTTT->szText));
            ::strcpy(pTTT->szText, strTip);

            // Set the text color to same color as circle
            m_ToolTip.SendMessage(TTM_SETTIPTEXTCOLOR, Color, 0L);
        }
        else
        {
            pTTT->szText[0] = 0;
        }
        bHandledNotify = TRUE;
    }
    return bHandledNotify;
}

BOOL CDTDemoView::PreTranslateMessage(MSG* pMsg) 
{
    if (::IsWindow(m_ToolTip.m_hWnd) && pMsg->hwnd == m_hWnd)
    {
        switch(pMsg->message)
        {
        case WM_LBUTTONDOWN:    
        case WM_MOUSEMOVE:
        case WM_LBUTTONUP:    
        case WM_RBUTTONDOWN:
        case WM_MBUTTONDOWN:    
        case WM_RBUTTONUP:
        case WM_MBUTTONUP:
            m_ToolTip.RelayEvent(pMsg);
            break;
        }
    }
    return CView::PreTranslateMessage(pMsg);
}

void CDTDemoView::OnMouseMove(UINT nFlags, CPoint point) 
{
    if (::IsWindow(m_ToolTip.m_hWnd))
    {
        const CCircle* pCircleHit = HitTest(point);

        if (!pCircleHit || pCircleHit != m_pCircleHit)
        {
            // Use Activate() to hide the tooltip.
            m_ToolTip.Activate(FALSE);        
        }

        if (pCircleHit)
        {
            m_ToolTip.Activate(TRUE);
            m_pCircleHit = pCircleHit;
        }
    }
    CView::OnMouseMove(nFlags, point);
}

CDTDemoView::OnInitialUpdate creates the DataTip and prepares it for use. I call m_ToolTip.Create and pass TTS_ALWAYSTIP so that the DataTip displays whether the application is active or not. I then call m_ToolTip.AddTool and pass CDTDemoView as the window that contains the tool. Because CToolTipCtrl::AddTool has default parameters of LPSTR_TEXTCALLBACK and lpRectTool set to NULL and nIDTool set to zero, the entire window will be considered a tool and the TTN_NEEDTEXT notification message will be sent to CDTDemoView. This notification allows me to set the text of the DataTip as in CDTDemoView::OnToolTipNeedText.

I send several messages to the DataTip to prepare it for use. TTM_SETMAXTIPWIDTH is sent with a large value (SHRT_MAX) in lParam for the maximum tip width. This forces the ToolTip to respect newline characters in a string (a new feature with the IE 4.0 Common Controls DLL). Next, I send the TTM_SETDELAYTIME message three times. The first time wParam is set to TTDT_AUTOPOP, which represents the time before a ToolTip disappears. It is set to a large (SHRT_MAX) value in lParam. This, in effect, turns off autopop so I can control when the DataTip disappears. The second time wParam is set to TTDT_INITIAL, which is the delay time between when the cursor stops moving in a circle and the DataTip first appears. This is set to 200 milliseconds so it comes up quickly. And finally, TTDT_RESHOW sets the delay time before another ToolTip window is displayed when the cursor is moved from one circle to another. It is also set to 200 milliseconds. Why didn't I just use CToolTipCtrl::SetDelayTime to set the delay times? Unfortunately, CToolTipCtrl::SetDelayTime does not allow you to set any delay time except TTDT_AUTOMATIC.

CDTDemoView::OnDraw and CDTDemoView::HitTest are fairly straightforward. CDTDemoView::OnDraw iterates through the collection of circles. For each circle, it calls CCircle::Draw. CDTDemoView::HitTest checks to see if the passed point is over a circle. It does this by iterating over the array of circles and calling CCircle::HitTest. Notice that the hit-testing is done in reverse order. This is the opposite of CDTDemoView::OnDraw, which is necessary in order to deal with the z-order of the circles. For example, if circle B is drawn on top of circle A, then to hit-test properly you must check circle B first.

CDTDemoView::OnToolTipNeedText intercepts the TTN_NEEDTEXT notification message from m_ToolTip. Its main responsibility is to perform hit-testing and adjust the DataTip text. It first gets the current cursor position and converts the position to client coordinates. It does a sanity test to see if the mouse is in the client area of the view. If I don't do this, I could break regular toolbar ToolTips because the main frame needs to get TTN_NEEDTEXT notifications for toolbar processing. CDTDemoView::OnToolTipNeedText will automatically get all TTN_NEEDTEXT messages if the view is active. The bHandledNotify variable indicates whether the message should be passed up to the main frame. If the cursor is in the client area, I check to see if a circle has been hit and store the results of the test in the m_pCircleHit member variable. This variable is also used in CDTDemoView::OnMouseMove. If a circle has been hit, I create a multiline string with the center, radius, and color values of the circle. This string is copied into the szText member of the TOOLTIPTEXT structure that is passed. Finally, I set the color of the DataTip text by sending a TTM_SETTIPTEXTCOLOR message. The wParam is the new color for the text and it is set to the same color as the circle (another feature from the IE 4.0 Common Controls DLL). If the circle has not been hit, szText is set to an empty string.

CDTDemoView::PreTranslateMessage relays the mouse events of interest to the DataTip. It does this by calling CToolTipCtrl::RelayEvent.CDTDemoView::PreTranslateMessage is called for every message that is posted to this window. It is easier to call CToolTipCtrl::RelayEvent here because I don't have to override every mouse event to relay that mouse event to the DataTip. This is similar to the CWnd's ToolTip implementation.

CDTDemoView::OnMouseMove is used to hide and show the DataTip based on the hit-testing of the circle. It calls the HitTest member function to see if the cursor is over a circle. If the cursor is not over a circle or if it is over a different circle than last time CDTDemoView displayed a DataTip, then CDTDemoView::OnMouseMove hides the DataTip by calling m_ToolTip.Activate(FALSE). The FALSE indicates that the DataTip should be turned off and hidden. Next, if a circle has been hit, I immediately reactivate the DataTip by calling m_ToolTip.Activate(TRUE) and set CDTDemoView::m_pCircleHit to the new circle. So, when the circle has changed I deactivate the circle and then immediately reactivate it. I do this to force the DataTip to ask for new text through TTN_NEEDTEXT. This allows me to update the text with the new information for the circle. Incidentally, IE 4.0 is supposed to have a TTM_POP message that will hide the ToolTip, but this message was not available in the version of the commctrl.h file that I had.

Home Brew Tips: Adding TitleTips

TitleTips are tips that elongate a truncated item in a control so that you can see all of the item. For instance, Visual C++ 4.x has TitleTips in its Project Workspace. If a class name in the Project Workspace is too long to see all of it, a TitleTip appears that displays the full text. This makes it possible to use the Project Workspace without scrolling horizontally and without having to make it wider. I created a demo that makes TitleTips for a listbox. However, you could use similar techniques to add TitleTips to other types of controls as well. The code that I wrote can work with either regular (non-owner-draw) listboxes or owner-draw listboxes. I filled both listboxes with some of my favorite programming and software engineering books (see Figure 9).

Figure 9 TitleTip sample application

You might think that I could use the ToolTip's owner-draw feature (made available with the IE 4.0 Common Controls DLL) for implementing TitleTips. However, the width of the ToolTip window is calculated based on the text width of the displayed item. In other words, you do not have direct control over the width of the ToolTip control. This doesn't work well in an owner-draw listbox because you may want to display something other than text. Besides, I think it is important to know how to create a tip from scratch because there is always a chance that the standard tip won't provide some functionality that you require for your application. For instance, you might want to create an animated or speaking tip.

Figure 10 contains a Booch class diagram that shows the relationship between the classes relevant to this example. The CListBox class is a standard MFC class that encapsulates listbox functionality. The CTitleTipListBox class is derived from CListBox and is responsible for creating and managing the TitleTip for the listbox. CTitleTipListBox can be used directly if you are implementing a regular listbox. The CTitleTip class is derived from CWnd and represents the actual TitleTip. The CODListBox class is an owner-draw listbox that inherits from CTitleTipListBox. If you want to create an owner-draw listbox, you must derive it from CTitleTipListBox and override CTitleTipListBox::GetIdealItemRect. I'll discuss CTitleTipListBox::GetIdealItemRect in more detail later.

Figure 10 Class Diagram for TitleTip Demo

The CTitleTip class is a window that represents the TitleTip (see Figure 11). CTitleTip::m_pszWndClass is a static data member used to hold the registered class name for the window. It is static because it only needs to be registered once for all instances of CTitleTip. CTitleTip::m_nItemIndex is the index of the listbox item that is currently displayed. This can be the constant CTitleTip::m_nNoIndex if no item is displayed. CTitleTip::m_pListBox is the parent of the TitleTip. The parent must be a listbox because I need to retrieve information from it to display the TitleTip.

Figure 11 CTitleTip

/////////////////////////////////////////////////////////////////////////////
// CTitleTip window

class CTitleTip : public CWnd
{
public:
    CTitleTip();

    virtual BOOL Create(CListBox* pParentWnd);

    virtual void Show(CRect DisplayRect, int nItemIndex);
    virtual void Hide();

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CTitleTip)
    //}}AFX_VIRTUAL

// Implementation
public:
    virtual ~CTitleTip();

protected:
    const int m_nNoIndex;        // Not a valid index
    static LPCSTR m_pszWndClass; // Registered class name
    int m_nItemIndex;            // Index of currently displayed listbox item
    CListBox* m_pListBox;        // Parent listbox

    BOOL IsListBoxOwnerDraw();

    // Generated message map functions
protected:
    //{{AFX_MSG(CTitleTip)
    afx_msg void OnPaint();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////
// TitleTip.cpp : implementation file
//

#include "stdafx.h"
#include "TitleTip.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CTitleTip

LPCSTR CTitleTip::m_pszWndClass = NULL;

CTitleTip::CTitleTip()
:m_nNoIndex(-1)
{
    // Register the window class if it has not already been registered by
    // previous instantiation of CTitleTip.
    if (m_pszWndClass == NULL)
    {
        m_pszWndClass = AfxRegisterWndClass(
            CS_SAVEBITS | CS_HREDRAW | CS_VREDRAW);
    }
    m_nItemIndex = m_nNoIndex;
    m_pListBox = NULL;
}

CTitleTip::~CTitleTip()
{
}

BOOL CTitleTip::Create(CListBox* pParentWnd)
{
    ASSERT_VALID(pParentWnd);
    m_pListBox = pParentWnd;

    // Don't add border to regular (non owner-draw) listboxes because
    // owner-draw item automatically adds border.
    DWORD dwStyle = WS_POPUP;
    if (!IsListBoxOwnerDraw())
    {
        dwStyle |= WS_BORDER;
    }

    return CreateEx(0, m_pszWndClass, NULL, 
        dwStyle, 0, 0, 0, 0,
        pParentWnd->GetSafeHwnd(), NULL, NULL);
}

BOOL CTitleTip::IsListBoxOwnerDraw()
{
    ASSERT_VALID(m_pListBox);
    DWORD dwStyle = m_pListBox->GetStyle();
    return (dwStyle & LBS_OWNERDRAWFIXED) || (dwStyle & LBS_OWNERDRAWVARIABLE);
}

void CTitleTip::Show(CRect DisplayRect, int nItemIndex)
{
    ASSERT_VALID(m_pListBox);
    ASSERT(nItemIndex < m_pListBox->GetCount()); 
    ASSERT(nItemIndex >= 0);
    ASSERT(::IsWindow(m_hWnd));
    ASSERT(!DisplayRect.IsRectEmpty());

    // Invalidate if new item.
    if (m_nItemIndex != nItemIndex)
    {
        m_nItemIndex = nItemIndex;
        InvalidateRect(NULL);
    }

    // Adjust window position and visibility.
    CRect WindowRect;
    GetWindowRect(WindowRect);
    int nSWPFlags = SWP_SHOWWINDOW | SWP_NOACTIVATE;
    if (WindowRect == DisplayRect)
    {
        nSWPFlags |= SWP_NOMOVE | SWP_NOSIZE;
    }
    VERIFY(SetWindowPos(&wndTopMost, DisplayRect.left, DisplayRect.top,
                        DisplayRect.Width(), DisplayRect.Height(), nSWPFlags));
}

void CTitleTip::Hide()
{
    ASSERT(::IsWindow(m_hWnd));
    ShowWindow(SW_HIDE);
}


BEGIN_MESSAGE_MAP(CTitleTip, CWnd)
    //{{AFX_MSG_MAP(CTitleTip)
    ON_WM_PAINT()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()


/////////////////////////////////////////////////////////////////////////////
// CTitleTip message handlers

void CTitleTip::OnPaint() 
{
    ASSERT(m_nItemIndex != m_nNoIndex);

    CPaintDC DC(this);

    int nSavedDC = DC.SaveDC();

    CRect ClientRect;
    GetClientRect(ClientRect);

    if (IsListBoxOwnerDraw())
    {
        // Let the listbox do the real drawing.
        DRAWITEMSTRUCT DrawItemStruct;

        DrawItemStruct.CtlType = ODT_LISTBOX;
        DrawItemStruct.CtlID = m_pListBox->GetDlgCtrlID();
        DrawItemStruct.itemID = m_nItemIndex;
        DrawItemStruct.itemAction = ODA_DRAWENTIRE;
        DrawItemStruct.hwndItem = m_pListBox->GetSafeHwnd();
        DrawItemStruct.hDC = DC.GetSafeHdc();
        DrawItemStruct.rcItem = ClientRect;
        DrawItemStruct.itemData = m_pListBox->GetItemData(m_nItemIndex);
        DrawItemStruct.itemState = (m_pListBox->GetSel(m_nItemIndex) > 0 ? 
                                    ODS_SELECTED : 0);
        if (m_pListBox->GetStyle() & LBS_MULTIPLESEL)
        {
            if (m_pListBox->GetCaretIndex() == m_nItemIndex)
            {
                DrawItemStruct.itemState |= ODS_FOCUS;
            }
        }
        else
        {
            DrawItemStruct.itemState |= ODS_FOCUS;
        }

        m_pListBox->DrawItem(&DrawItemStruct);
    }
    else
    {
        // Do all of the drawing ourselves
        CFont* pFont = m_pListBox->GetFont();
        ASSERT_VALID(pFont);
        DC.SelectObject(pFont);

        COLORREF clrBackground = RGB(255, 255, 255);
        if (m_pListBox->GetSel(m_nItemIndex) > 0)
        {
            DC.SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
            clrBackground = ::GetSysColor(COLOR_HIGHLIGHT);
        }

        // Draw background
        DC.FillSolidRect(ClientRect, clrBackground);

        // Draw text of item
        CString strItem;
        m_pListBox->GetText(m_nItemIndex, strItem);
        ASSERT(!strItem.IsEmpty());
        DC.SetBkMode(TRANSPARENT);
        DC.TextOut(1, -1, strItem);
    }

    DC.RestoreDC(nSavedDC);

    // Do not call CWnd::OnPaint() for painting messages
}

CTitleTip::CTitleTip registers a new window class by calling AfxRegisterWndClass and storing the class name in CTitleTip::m_pszWndClass. I call AfxRegisterWndClass so I can register a window class with the CS_SAVEBITS class style flag. The CS_SAVEBITS class style flag is an optimization that causes Windows to save the portion of any window obscured by the TitleTip as a bitmap. As a result, Windows does not need to send a WM_PAINT message to the window underneath the TitleTip when the TitleTip disappears. CTitleTip::Create creates the TitleTip as a popup window. It only adds a border to the TitleTip window if the listbox is a regular listbox because Windows automatically adds borders to owner-draw listboxes before it sends WM_DRAWITEM. Notice that CTitleTip::m_pszWndClass is passed as the window class name to CWnd::CreateEx. CTitleTip::IsListBoxOwnerDraw returns TRUE if the parent listbox is owner-draw. It figures this out by interrogating the style bits on the listbox.

CTitleTip::Show is responsible for showing the TitleTip. The DisplayRect parameter is the location and size of the TitleTip in parent client coordinates. The nItemIndex parameter is the listbox item index to be displayed. I optimized the routine so it only invalidates and adjusts the size and position of the TitleTip if it has changed. CWnd::SetWindowPos is called to adjust the TitleTip window. The first parameter is set to wndTopMost so that the TitleTip will always be on top of all other windows. The SWP_NOACTIVATE flag is passed so that the window does not get the focus. Since the TitleTip does not need any keyboard input, it never needs to get the focus. CTitleTip::Hide hides the TitleTip by calling CWnd::ShowWindow with SW_HIDE.

CTitleTip::OnPaint draws the TitleTip differently if the parent listbox is owner-draw versus regular. If the parent listbox is owner-draw, it creates and initializes a DrawItemStruct similar to how Windows initializes the DrawItemStruct when it is going to send WM_DRAWITEM. The one important difference is that, instead of initializing the hDC structure member to the device context of the listbox, it initializes the hDC to the device context of the TitleTip window. It then calls m_pListBox->DrawItem, passing the address of the DrawItemStruct. The net result is that the listbox actually draws the item into the TitleTip window. Pretty clever! This is the advantage of object-oriented programming and well-defined interfaces. The listbox doesn't know or care where it is actually drawing the item—it just knows how to draw it. CTitleTip does not know how to draw the owner-draw item, but it knows how to properly initialize DrawItemStruct and call CListBox::DrawItem. On the other hand, if the parent listbox is a regular listbox, the CTitleTip does all of the drawing itself. Fortunately, this is not too hard. It gets the appropriate text and font from the parent listbox, adjusts the device context, fills the background, and draws the text.

CTitleTipListBox is responsible for managing and displaying the TitleTip (see Figure 12). CTitleTipListBox::m_ LastMouseMovePoint is the last known position of mouse. CTitleTipListBox::m_bMouseCaptured indicates whether the CTitleTipListBox is currently capturing the mouse. CTitleTipListBox::m_TitleTip is an instance of CTitleTip that gets displayed. CTitleTipListBox::m_nNoIndex is a constant used to indicate that nothing is selected in the listbox.

Figure 12 CTitleTipListBox

// TitleTipListBox.h : header file
//

/////////////////////////////////////////////////////////////////////////////
// CTitleTipListBox window

#ifndef __TITLETIPLISTBOX_H__
#define __TITLETIPLISTBOX_H__

#include "TitleTip.h"

class CTitleTipListBox : public CListBox
{
// Construction
public:
    CTitleTipListBox();

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CTitleTipListBox)
    public:
    virtual BOOL PreTranslateMessage(MSG* pMsg);
    //}}AFX_VIRTUAL

// Implementation
public:
    virtual ~CTitleTipListBox();

protected:
    const int m_nNoIndex; // Not a valid index
    CPoint m_LastMouseMovePoint; // Last position of mouse cursor
    BOOL m_bMouseCaptured; // Is mouse captured?
    CTitleTip m_TitleTip; // TitleTip that gets displayed when necessary

    // This method should be overridden by an owner-draw listbox.
    virtual int GetIdealItemRect(int nIndex, LPRECT lpRect);

    void AdjustTitleTip(int nNewIndex);
    void CaptureMouse();
    BOOL IsAppActive();

    // Generated message map functions
protected:
    //{{AFX_MSG(CTitleTipListBox)
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
    afx_msg void OnSelchange();
    afx_msg void OnKillFocus(CWnd* pNewWnd);
    afx_msg void OnDestroy();
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
    //}}AFX_MSG
    afx_msg LONG OnContentChanged(UINT, LONG);

    DECLARE_MESSAGE_MAP()
};

#endif // __TITLETIPLISTBOX_H__

/////////////////////////////////////////////////////////////////////////////
// TitleTipListBox.cpp : implementation file
//

#include "stdafx.h"
#include "TitleTipListBox.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CTitleTipListBox

CTitleTipListBox::CTitleTipListBox()
: m_LastMouseMovePoint(0, 0) , m_nNoIndex(-1)
{
    m_bMouseCaptured = FALSE;
}

CTitleTipListBox::~CTitleTipListBox()
{
    ASSERT(!m_bMouseCaptured);
}

int CTitleTipListBox::GetIdealItemRect(int nIndex, LPRECT lpRect)
{
    // Calculate the ideal rect for an item. The ideal rect is dependent
    // on the length of the string. This only works for regular 
    // (non owner-draw)listboxes.
    ASSERT(lpRect);
    ASSERT(nIndex >= 0);
    DWORD dwStyle = GetStyle();
    int    nStatus = GetItemRect(nIndex, lpRect);    
    if (nStatus != LB_ERR && !(dwStyle & LBS_OWNERDRAWFIXED) && 
        !(dwStyle & LBS_OWNERDRAWVARIABLE))
    {
        CString strItem;
        GetText(nIndex, strItem);
        if (!strItem.IsEmpty())
        {
            // Calulate the ideal text length.
            CClientDC DC(this);
            CFont* pOldFont = DC.SelectObject(GetFont());
            CSize ItemSize = DC.GetTextExtent(strItem);
            DC.SelectObject(pOldFont);

            // Take the maximum of regular width and ideal width.
            const int cxEdgeSpace = 2;
            lpRect->right = max(lpRect->right, 
                lpRect->left + ItemSize.cx + (cxEdgeSpace * 2));
        }
    }
    else
    {
        TRACE("Owner-draw listbox detected - override CTitleTipListBox::GetIdeaItemRect()\n");
    }
    return nStatus;
}

void CTitleTipListBox::AdjustTitleTip(int nNewIndex)
{
    if (!::IsWindow(m_TitleTip.m_hWnd))
    {
        VERIFY(m_TitleTip.Create(this));
    }

    if (nNewIndex == m_nNoIndex)
    {
        m_TitleTip.Hide();
    }
    else
    {
        CRect IdealItemRect;
        GetIdealItemRect(nNewIndex, IdealItemRect);
        CRect ItemRect;
        GetItemRect(nNewIndex, ItemRect);
        if (ItemRect == IdealItemRect)
        {
            m_TitleTip.Hide();
        }
        else
        {
            // Adjust the rect for being near the edge of screen.
            ClientToScreen(IdealItemRect);
            int nScreenWidth = ::GetSystemMetrics(SM_CXFULLSCREEN);
            if (IdealItemRect.right > nScreenWidth)
            {
                IdealItemRect.OffsetRect(nScreenWidth - IdealItemRect.right, 0);
            }
            if (IdealItemRect.left < 0)
            {
                IdealItemRect.OffsetRect(-IdealItemRect.left, 0);
            }

            m_TitleTip.Show(IdealItemRect, nNewIndex);  
        }
    }

    if (m_TitleTip.IsWindowVisible())
    {
        // Make sure we capture mouse so we can detect when to turn off 
        // title tip.
        if (!m_bMouseCaptured && GetCapture() != this)
        {
            CaptureMouse();
        }
    }
    else
    {
        // The tip is invisible so release the mouse.
        if (m_bMouseCaptured)
        {
            VERIFY(ReleaseCapture());
            m_bMouseCaptured = FALSE;
        }
    }
}

void CTitleTipListBox::CaptureMouse()
{
    ASSERT(!m_bMouseCaptured);
    CPoint Point;
    VERIFY(GetCursorPos(&Point));
    ScreenToClient(&Point);
    m_LastMouseMovePoint = Point;
    SetCapture();
    m_bMouseCaptured = TRUE;
}

/////////////////////////////////////////////////////////////////////////////
// CTitleTipListBox message handlers


LONG CTitleTipListBox::OnContentChanged(UINT, LONG)
{
    // Turn off title tip.
    AdjustTitleTip(m_nNoIndex);
    return Default();
}


void CTitleTipListBox::OnMouseMove(UINT nFlags, CPoint point) 
{
    if (point != m_LastMouseMovePoint && IsAppActive())
    {
        m_LastMouseMovePoint = point;

        int nIndexHit = m_nNoIndex;

        CRect ClientRect;
        GetClientRect(ClientRect);
        if (ClientRect.PtInRect(point))
        {
            // Hit test.
            for (int n = 0; nIndexHit == m_nNoIndex && n < GetCount(); n++)
            {
                CRect ItemRect;
                GetItemRect(n, ItemRect);
                if (ItemRect.PtInRect(point))
                {
                    nIndexHit = n;    
                }
            }
        }
        AdjustTitleTip(nIndexHit);
    }
    CListBox::OnMouseMove(nFlags, point);
}


void CTitleTipListBox::OnSelchange() 
{
    int nSelIndex;
    if (GetStyle() & LBS_MULTIPLESEL)
    {
        nSelIndex = GetCaretIndex();    
    }
    else
    {
        nSelIndex = GetCurSel();
    }
    AdjustTitleTip(nSelIndex);
    m_TitleTip.InvalidateRect(NULL);
    m_TitleTip.UpdateWindow();
}

void CTitleTipListBox::OnKillFocus(CWnd* pNewWnd) 
{
    CListBox::OnKillFocus(pNewWnd);
    if (pNewWnd != &m_TitleTip)
    {
        AdjustTitleTip(m_nNoIndex);
    }
}

void CTitleTipListBox::OnDestroy() 
{
    AdjustTitleTip(m_nNoIndex);
    m_TitleTip.DestroyWindow();
    CListBox::OnDestroy();
}

void CTitleTipListBox::OnLButtonDown(UINT nFlags, CPoint point) 
{
    // Temporarily disable mouse capturing because the base class may 
    // capture the mouse.

    if (m_bMouseCaptured)
    {
        ReleaseCapture();
        m_bMouseCaptured = FALSE;
    }

    CListBox::OnLButtonDown(nFlags, point);

    if (m_TitleTip.IsWindowVisible())
    {
        m_TitleTip.InvalidateRect(NULL);
        if (this != GetCapture())
        {
            CaptureMouse();
        }
    }
}

void CTitleTipListBox::OnLButtonUp(UINT nFlags, CPoint point) 
{
    CListBox::OnLButtonUp(nFlags, point);

    if (this != GetCapture() && m_TitleTip.IsWindowVisible())
    {
        CaptureMouse();
    }
}


BOOL CTitleTipListBox::PreTranslateMessage(MSG* pMsg) 
{
    switch (pMsg->message)
    {
        case WM_RBUTTONDOWN:
        case WM_RBUTTONUP:
        case WM_LBUTTONDBLCLK:
        case WM_RBUTTONDBLCLK:
            // Make the active view because that is the default
            // behaviour caused by WM_MOUSEACTIVATE when NO TitleTip
            // is over this window.
            AdjustTitleTip(m_nNoIndex);
            CFrameWnd* pFrameWnd = GetParentFrame();
            if (pFrameWnd)
            {
                BOOL bDone = FALSE;
                CWnd* pWnd = this;
                while (!bDone)
                {
                    pWnd = pWnd->GetParent();
                    if (!pWnd || pWnd == pFrameWnd)
                    {
                        bDone = TRUE;
                    }
                    else if (pWnd->IsKindOf(RUNTIME_CLASS(CView)))
                    {
                        pFrameWnd->SetActiveView((CView*)pWnd);
                        bDone = TRUE;
                    }
                }
            }
            break;
    }
    
    return CListBox::PreTranslateMessage(pMsg);
}

CTitleTipListBox::GetIdealItemRect calculates the ideal item size and position. The nIndex parameter is the index of the item you want. The lpRect-passed parameter is used to return the ideal size and position in client coordinates. You must override this method if your listbox is an owner-draw listbox, and I'll show you how CODListBox overrides this method later. If you don't override this method and your listbox is owner-draw, then CTitleTipListBox::GetIdealItemRect emits a TRACE statement. However, if the listbox is a regular listbox, this method automatically calculates the ideal item size and position. It first calls CListBox::GetItemRect to calculate the height and width of the item. The width of the item returned by CListBox::GetItemRect is the width of the listbox, not the width of the text. To calculate the actual width of the text, I get the text and font for the item and then call CDC::GetTextExtent. Finally, I set lpRect to the maximum of the item width or the calculated text width (plus some space around the edges for aesthetic reasons).

CTitleTipListBox::AdjustTitleTip displays or hides the TitleTip as necessary. The nNewIndex parameter is the index of the listbox item to display. This can be the constant m_nNoIndex if no item is to be displayed. It creates a TitleTip if one has not already been created. If the new index is m_nNoIndex, then it hides the TitleTip. Otherwise, it gets the ideal item rect by calling CTitleTipListBox::GetIdealItemRect. If the ideal item rectangle is the same as the rectangle returned by CListBox::GetItemRect, there is no need for a TitleTip so it hides the TitleTip. Otherwise, it adjusts the ideal item rectangle so that it will fit within the screen and shows the TitleTip. If a TitleTip is visible, I capture the mouse so that I know when to hide the TitleTip. In other words, if the user moves the cursor so that it is not over any item, I need to hide the TitleTip; if the TitleTip is not visible, then I release the capture of the cursor. CTitleTipListBox::CaptureMouse captures the cursor. It stores the position of the cursor in client coordinates in CTitleTipListBox::m_LastMouseMovePoint. It also sets the m_bMouseCaptured flag to TRUE to indicate that the cursor is now captured.

CTitleTipListBox::IsAppActive returns TRUE if the application that contains the listbox is active. It determines this by getting the active window and testing to see if it is the same as, or a child of, the top-level parent of the application. This method is used in CTitleTipListBox::OnMouseMove to make certain that the TitleTip is displayed only when the application is active.

CTitleTipListBox::OnContentChanged hides the TitleTip and is called when various events occur that could change the contents of the listbox. For instance, the LB_INSERTSTRING message, which inserts a string in a listbox, could make the displayed TitleTip invalid because the cursor would be over a different string after the insertion. The message map shows what particular events are handled through the use of the ON_MESSAGE macro. Wondering why I didn't use CWnd::PreTranslateMessage to intercept these messages? Well, I tried, but CWnd::PreTranslateMessage only intercepts messages from the message queue. These intercepted messages appear to be the result of Windows calling SendMessage, which bypasses the message queue.

CTitleTipListBox::OnMouseMove does hit-testing to see if a TitleTip needs to be displayed. It only does hit-testing if the application that contains the listbox is active and the cursor has actually moved from its last position. I found that Windows sometimes sends multiple WM_MOUSEMOVE messages for the same cursor position, so I use the m_LastMouseMovePosition data member to filter out these unnecessary messages. CTitleTipListBox::OnMouseMove then does a check to make certain that the cursor is in the client area of listbox. The cursor could be outside the client area because I'm capturing the mouse. An amusing side-effect of not doing this test is that the TitleTip would sometimes appear for items that aren't currently visible in the listbox. Anyway, if the mouse is in the client area of the listbox, CTitleTipListBox::OnMouseMove iterates over all the items and tests to see if the cursor is over an item. If so, it uses that item as the new index to pass to CTitleTipListBox::AdjustTitleTip.

CTitleTipListBox::OnSelchange handles the LBN_SELCHANGE notification message from windows. If the selection has changed, then it may need to adjust the TitleTip to reflect the changed selection state. For example, if the new selection is the same as the item that is being displayed by the TitleTip, then the TitleTip needs to be updated to reflect a selected item. Notice that CTitleTipListBox::OnSelchange respects whether the listbox is multiple or single selection. In the case of multiple selection, it calls CListBox::GetCaretIndex. In the case of single selection, it calls CListBox::GetCurSel. Handling the LBN_SELCHANGE notification message also makes it possible for TitleTips to be displayed properly when the user selects items with the keyboard instead of the mouse.

CTitleTipListBox::OnKillFocus and CTitleTipListBox::OnDestroy are relatively simple. CTitleTipListBox::OnKillFocus hides the TitleTip unless the new focus window is the TitleTip window. This is necessary so that the TitleTip is automatically hidden when the user tabs away from the listbox. CTitleTipListBox::OnDestroy hides and destroys the TitleTip.

CTitleTipListBox::OnLButtonDown invalidates the TitleTip so it can be redrawn in case the selection state has changed. I temporarily turn off capturing before calling the base class because I found that I broke selection tracking if I didn't. Selection tracking occurs when you drag the selection rectangle from listbox item to listbox item. Since I'm not privy to the listbox internals, I can only speculate as to the cause of the problem. Maybe the listbox control needs to capture the mouse itself in order to support selection changes on left mouse-button down.

CTitleTipListBox::OnLButtonUp captures the mouse if the TitleTip window is visible and CTitleTipListBox is not already capturing the mouse. CTitleTipListBox::PreTranslateMessage watches for other types of mouse button messages and makes the view active if the listbox is in a view. I did this to mimic the default behavior of an MFC view when it gets a WM_MOUSEACTIVATE message. Otherwise, it could miss a mouse activate message when the user clicks on the TitleTip window.

CODListBox is an example of an owner-draw listbox with TitleTips (see Figure 13). CODListBox::m_nEdgeSpace is a constant used to add extra space around text. CODListBox::m_nFontHeight is a constant that represents the desired height of the font used for displaying items. CODListBox::m_Font is the font used for displaying an item. CODListBox::CODListBox creates the font (m_Font) for drawing in a listbox.

Figure 13 CODListBox

// ODListBox.h : header file
//

/////////////////////////////////////////////////////////////////////////////
// CODListBox window

#include "TitleTipListBox.h"

class CODListBox : public CTitleTipListBox
{
// Construction
public:
    CODListBox();

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CODListBox)
    public:
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
    virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
    //}}AFX_VIRTUAL

// Implementation
public:
    virtual ~CODListBox();

protected:
    const int m_nEdgeSpace; // Extra space surrounding text
    const int m_nFontHeight; // Height of font
    CFont m_Font; // Font used for displaying item

    virtual int GetIdealItemRect(int nIndex, LPRECT lpRect);

    // Generated message map functions
protected:
    //{{AFX_MSG(CODListBox)
        // NOTE - the ClassWizard will add and remove member functions here.
    //}}AFX_MSG

    DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////
// ODListBox.cpp : implementation file
//

#include "stdafx.h"
#include "TTDemo.h"
#include "ODListBox.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CODListBox

CODListBox::CODListBox()
: m_nEdgeSpace(4), m_nFontHeight(20) 
{
    VERIFY(m_Font.CreateFont(m_nFontHeight, 0, 0, 0, FW_BOLD, 0, 0, 0, 
                             ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, 
                             DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, 
                             "Arial"));
}

CODListBox::~CODListBox()
{
}

int CODListBox::GetIdealItemRect(int nIndex, LPRECT lpRect)
{
    ASSERT(nIndex >= 0);

    int nResult = GetItemRect(nIndex, lpRect);

    if (nResult != LB_ERR)
    {
        CClientDC DC(this);
        CFont* pOldFont = DC.SelectObject(&m_Font);

        // Calculate the text length.
        CString strItem;
        GetText(nIndex, strItem);
        CSize TextSize = DC.GetTextExtent(strItem);

        // Take the maximum of the regular or ideal.
        lpRect->right = max(lpRect->right, 
            lpRect->left + TextSize.cx + (m_nEdgeSpace * 2));

        DC.SelectObject(pOldFont);
    }
    return nResult;
}

BEGIN_MESSAGE_MAP(CODListBox, CTitleTipListBox)
    //{{AFX_MSG_MAP(CODListBox)
        // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CODListBox message handlers

void CODListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
{
    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    ASSERT_VALID(pDC);
    int nSavedDC = pDC->SaveDC();

    CString strItem;
    if (lpDrawItemStruct->itemID != -1)
    {
        GetText(lpDrawItemStruct->itemID, strItem);
    }

    COLORREF TextColor;
    COLORREF BackColor;
    UINT nItemState = lpDrawItemStruct->itemState; 
    if (nItemState & ODS_SELECTED)
    {
        TextColor = RGB(255, 255, 255); // White
        BackColor = RGB(255, 0, 0); // Red
    }
    else
    {
        TextColor = RGB(255, 0, 0);  // Red
        BackColor = RGB(255, 255, 255); // White
    }
    
    CRect ItemRect(lpDrawItemStruct->rcItem);

    // Draw background
    pDC->FillSolidRect(ItemRect, BackColor);

    // Draw text
    pDC->SetTextColor(TextColor);
    pDC->SetBkMode(TRANSPARENT);
    pDC->SelectObject(&m_Font);
    ItemRect.left += m_nEdgeSpace;
    pDC->DrawText(strItem, ItemRect, 
        DT_LEFT | DT_SINGLELINE | DT_VCENTER);
    ItemRect.left -= m_nEdgeSpace;

    // Draw focus rect if necessary
    if (nItemState & ODS_FOCUS)
    {
        pDC->DrawFocusRect(ItemRect);
    }

    pDC->RestoreDC(nSavedDC);
}

void CODListBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) 
{
    lpMeasureItemStruct->itemHeight = m_nFontHeight + (m_nEdgeSpace * 2);    
}

CODListBox::GetIdealItemRect overrides the CTitleTipListBox method of the same name. As you can see, the implementation is similar to the base class's implementation, except that it is using m_Font for the font. I actually could have made this work without overriding the base class if I had called CWnd::SetFont to set the font for the listbox. However, I wanted to show you how to override the method for other cases. For instance, if you want to display graphic images in the listbox, you would have to override CTitleTipListBox::GetIdealItemRect.

CODListBox::DrawItem draws the item based on the DrawItemStruct. This code is similar to the code found in CTitleTip::OnPaint, except that the colors are red and white instead of the default colors. Remember that CTitleTip may actually be calling this method to draw into its window.

CODListBox::MeasureItem calculates the height of an item based on font and desired edge space. This is only called once by Windows because the listbox style is LBS_OWNERDRAWFIXED. It may be called for each item in the listbox if the style is LBS_OWNERDRAWVARIABLE.

CTTDemoDlg holds the two listboxes, and most of the code was generated by AppWizard (see Figure 14). I added the m_RegListBox and m_ODListBox data members for the regular and owner-draw listboxes, respectively. The only other code that I added is in CTTDemoDlg::OnInitDialog, where I subclassed both listboxes by calling CWnd::SubclassWindow. I also loaded both listboxes from a static array, pszItemArray.

Figure 14 CTTDemoDlg

// TTDemoDlg.h : header file
/////////////////////////////////////////////////////////////////////////////
// CTTDemoDlg dialog

#include "TitleTipListBox.h"
#include "ODListBox.h"

class CTTDemoDlg : public CDialog
{
// Construction
public:
    CTTDemoDlg(CWnd* pParent = NULL);    // standard constructor

// Dialog Data
    //{{AFX_DATA(CTTDemoDlg)
    enum { IDD = IDD_TTDEMO_DIALOG };
        // NOTE: the ClassWizard will add data members here
    //}}AFX_DATA
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CTTDemoDlg)
    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
    //}}AFX_VIRTUAL

// Implementation
protected:
    HICON m_hIcon;
    CTitleTipListBox m_RegListBox; // Regular listbox
    CODListBox m_ODListBox; // Owner-draw listbox

    // Generated message map functions
    //{{AFX_MSG(CTTDemoDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

// TTDemoDlg.cpp : implementation file

#include "stdafx.h"
#include "TTDemo.h"
#include "TTDemoDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CTTDemoDlg dialog

CTTDemoDlg::CTTDemoDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CTTDemoDlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(CTTDemoDlg)
        // NOTE: the ClassWizard will add member initialization here
    //}}AFX_DATA_INIT
    // Note that LoadIcon does not require a subsequent DestroyIcon in Win32
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CTTDemoDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CTTDemoDlg)
        // NOTE: the ClassWizard will add DDX and DDV calls here
    //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CTTDemoDlg, CDialog)
    //{{AFX_MSG_MAP(CTTDemoDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CTTDemoDlg message handlers

BOOL CTTDemoDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // Set the icon for this dialog.  The framework does this automatically
    //  when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);            // Set big icon
    SetIcon(m_hIcon, FALSE);        // Set small icon

    // Subclass regular listbox
    HWND hwndRegListBox = ::GetDlgItem(GetSafeHwnd(), IDC_REGLISTBOX);
    ASSERT(hwndRegListBox);
    VERIFY(m_RegListBox.SubclassWindow(hwndRegListBox));

    // Subclass owner-draw listbox
    HWND hwndODListBox = ::GetDlgItem(GetSafeHwnd(), IDC_ODLISTBOX);
    ASSERT(hwndODListBox);
    VERIFY(m_ODListBox.SubclassWindow(hwndODListBox));

    // Load both listboxes with items
    static char* pszItemArray[] =
    {
        "The C++ Programming Language",
        "C++ Primer",
        "OLE Controls Inside Out",
        "Inside OLE 2nd Edition",
        "Inside ODBC",
        "Code Complete",
        "Rapid Software Development",
        "The Design Of Everyday Things",
        "Object-Oriented Analysis And Design",
        "MFC Internals",
        "Animation Techniques In Win32",
        "Inside Visual C++",
        "Writing Solid Code",
        "Learn Java Now"
    };
    static int nItemArrayCount = sizeof(pszItemArray) / sizeof(pszItemArray[0]);
    for (int n = 0; n < nItemArrayCount; n++)
    {
        VERIFY(m_RegListBox.AddString(pszItemArray[n]) != LB_ERR);
        VERIFY(m_ODListBox.AddString(pszItemArray[n]) != LB_ERR);
    }

    return TRUE;  // return TRUE  unless you set the focus to a control
}

// If you add a minimize button to your dialog, you will need the code below
//  to draw the icon.  For MFC applications using the document/view model,
//  this is automatically done for you by the framework.

void CTTDemoDlg::OnPaint() 
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting
        SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // Draw the icon
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialog::OnPaint();
    }
}

// The system calls this to obtain the cursor to display while the user drags
//  the minimized window.
HCURSOR CTTDemoDlg::OnQueryDragIcon()
{
    return (HCURSOR) m_hIcon;
}

Wrap Up

Well there you have it—five tips on using tips. I hope this will inspire you to add more ToolTips, TitleTips, and DataTips to your applications and Web pages. Better yet, maybe you'll invent a new type of tip that all of us can use in our applications. If you do, make sure you pass it along to the rest of us!

Special thanks to Bill Kinsley and others at AM Communications, Inc.

Adding Delays to TitleTips

The TitleTip that I created does not include any of the delay features provided by the TTM_SETDELAYTIME message of a standard ToolTip. I didn't add any delays because Microsoft doesn't appear to have added any delays to its TitleTip implementation either. However, most other types of tips do have delays built in, so in case you want to add delays to your own custom tips, I want to explain how to add them.

In general, the delays involve the use of window timers. CWnd provides two functions related to timers: CWnd::SetTimer and CWnd::KillTimer. CWnd::SetTimer sets a timer and takes a timer ID, a timeout value in milliseconds, and a function pointer to a timer handler as parameters. If the function pointer is NULL, then it uses the WM_TIMER message to notify the window of a timer event. CWnd::KillTimer stops a timer and takes a timer ID of the timer to stop as a parameter.

Given this support for timers, let's see how you can add delays to a custom tip. I'm going to show you how to add the equivalent of the TTDT_AUTOPOP feature of the TTM_SETDELAYTIME message. This is the delay before the tip is automatically hidden if the cursor does not move.

First, add member variables to the protected or private section of the custom tip class to store the timer ID and the position of the cursor when the tip was displayed. Also, add a constant to store the delay time before the tip is automatically hidden. The code would look like this:

class CCustomTip: public CWnd
{
.
.
.
protected:
    const int m_nAutoPopDelay; // In milliseconds.
    UINT m_nTimerId;
    CPoint m_LastMousePosition;
.
.
.
};

Initialize the m_nAutoPopDelay to 10000 (ten seconds) and the m_nTimerId to zero in the constructor.

CCustomTip::CCustomTip
: m_nAutoPopDelay(10000), m_LastMousePosition(0, 0)
{
.
.
.
m_nTimerId = 0;
.
.
.
}

You can actually set the delay to whatever you want—I chose ten seconds for demonstration purposes.

Add code to start the timer when the tip is shown. For instance, if there was a CCustomTip::Show member function, you would add the following code to the method that shows the window:

void CCustomTip::Show
{
.
.
.
    if (m_nTimerId > 0)
    {
        VERIFY(KillTimer(m_nTimerId));
        m_nTimerId = 0;
    }
    VERIFY(::GetCursorPos(&m_LastMousePosition));
    m_nTimerId = SetTimer(1, m_nAutoPopDelay, NULL);
    ASSERT(m_nTimerId != 0);
.
.
.
}

Add code to handle the timer message. Use ClassWizard to add a WM_TIMER handler to the message map. The handler code should look like the following:

void CTitleTip::OnTimer(UINT nIDEvent)
{
    CPoint CurrentMousePosition;

    VERIFY(::GetCursorPos(&CurrentMousePosition));
    if (CurrentMousePosition == m_LastMousePosition)
    {
        Hide();
    }
    else
    {
        m_LastMousePosition = CurrentMousePosition;
    }
}

Finally, add code to the CCustomTip::Hide member to kill the timer:

void CCustomTip::Hide()
{
    if (m_nTimerId > 0)
    {
        VERIFY(KillTimer(m_nTimerId));
        m_nTimerId = 0;
    }
    ShowWindow(SW_HIDE);
}

You would use similar coding techniques to add the other types of delays built into standard ToolTips, but you would have to keep track of more state changes than just cursor position. For example, suppose you want to add the equivalent of the TTDT_RESHOW feature. This is the amount of time before another ToolTip is displayed when the cursor is moved from one item (or tool) to another. You would have to create member variables to keep track of when the last ToolTip was hidden and what it contained.

This article is reproduced from Microsoft Systems Journal. Copyright © 1997 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S. and Canada, or (303) 678-0439 in all other countries. For other inquiries, call (415) 905-2200.