Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CListCtrl and Displaying a Tooltip

0.00/5 (No votes)
12 Mar 2009 1  
Examples of how to implement tooltips in the MFC list control

Introduction

Microsoft's CListCtrl has support for displaying data in a grid, but requires a little help to display tooltips. This article will demonstrate how we can display tooltips when using a CListCtrl. The demo application allows you to experience the behavior of the different tooltip implementations.

screenshot.png

Background

There are lots of advanced grid controls that extend the CListCtrl, so it is possible to display a tooltip when holding the mouse over a cell. But, because these grid controls can be very complex, it can be difficult to see how they do it.

How To Implement a Tooltip in CListCtrl

For normal MFC controls, there are usually two methods for enabling tooltips:

  • Calling CWnd::EnableToolTips()
  • Adding CToolTipCtrl as a member variable

The CListCtrl has a few more tricks up its sleeve, which add some more options:

  • Has its own CToolTipCtrl member variable, which can be accessed with CListCtrl::GetToolTips()
  • Recognizes the extended style LVS_EX_LABELTIP, which activates the tooltip when the mouse hovers over a cell where the entire text is only partially displayed
  • Recognizes the extended style LVS_EX_INFOTIP, which enables tooltips for the label column

This article will only focus on how to display a simple tooltip for a cell in CListCtrl. Implementing tooltips is a rather large topic, which can be seen in the MSDN articles: About ToolTip Controls and Using ToolTip Controls.

CWnd::EnableToolTips()

This method usually requires three steps:

  1. Call CWnd::EnableToolTips(TRUE) during the control initialization, which for the CListCtrl means over-defining CWnd::PreSubclassWindow().
  2. Over-define CWnd::OnToolHitTest(...) for the CListCtrl. It will be called every time the mouse moves over the control, and the return value of the function specifies whether a tooltip should be displayed.
  3. Implement a TTN_NEEDTEXT message handler. It will be called when the mouse is over the control and the previous method has returned that a tooltip is available.

The third step is only necessary if the second step specifies that the tooltip text should be retrieved using the callback (LPSTR_TEXTCALLBACK). The reason for doing a callback is only speed consideration, in case the lookup of the tooltip text is slow. Instead of specifying a callback, we can just return the tooltip text directly by performing a malloc() to hold the text (it will be automatically deallocated) and enables tooltip text beyond 80 characters.

If using the TTN_NEEDTEXT message handler and one wants to display tooltip longer than 80 characters, then one must allocate the wanted text buffer and set the TOOLTIPTEXT::lpszText pointer to this text-buffer in the message handler (one has to deallocate this text buffer manually):

BEGIN_MESSAGE_MAP(CListCtrl_EnableToolTip, CListCtrl)
    ON_NOTIFY_EX(TTN_NEEDTEXTA, 0, OnToolNeedText)
    ON_NOTIFY_EX(TTN_NEEDTEXTW, 0, OnToolNeedText)
END_MESSAGE_MAP()

void CListCtrl_EnableToolTip::PreSubclassWindow()
{
    CListCtrl::PreSubclassWindow();

    // Disable the CToolTipCtrl of CListCtrl so it won't disturb the CWnd tooltip
    GetToolTips()->Activate(FALSE);

    // Activate the standard CWnd tooltip functionality
    VERIFY( EnableToolTips(TRUE) );
}

INT_PTR CListCtrl_EnableToolTip::OnToolHitTest(CPoint point, TOOLINFO * pTI) const
{
    CPoint pt(GetMessagePos());
    ScreenToClient(&pt);
    if (!ShowToolTip(pt))
        return -1;

    int nRow, nCol;
    CellHitTest(pt, nRow, nCol);

    //Get the client (area occupied by this control)
    RECT rcClient;
    GetClientRect( &rcClient );

    //Fill in the TOOLINFO structure
    pTI->hwnd = m_hWnd;
    pTI->uId = (UINT) (nRow * 1000 + nCol);
    // Send TTN_NEEDTEXT when tooltip should be shown
    pTI->lpszText = LPSTR_TEXTCALLBACK;
    pTI->rect = rcClient;

    return pTI->uId;
    // Must return a unique value for each cell (Marks a new tooltip)
}

BOOL CListCtrl_EnableToolTip::OnToolNeedText(UINT id, NMHDR* pNMHDR, LRESULT* pResult)
{
    CPoint pt(GetMessagePos());
    ScreenToClient(&pt);

    int nRow, nCol;
    CellHitTest(pt, nRow, nCol);

    CString tooltip = GetToolTipText(nRow, nCol);
    if (tooltip.IsEmpty())
        return FALSE;

    // Non-unicode applications can receive requests for tooltip-text in unicode
    TOOLTIPTEXTA* pTTTA = (TOOLTIPTEXTA*)pNMHDR;
    TOOLTIPTEXTW* pTTTW = (TOOLTIPTEXTW*)pNMHDR;
#ifndef _UNICODE
    if (pNMHDR->code == TTN_NEEDTEXTA)
        lstrcpyn(pTTTA->szText, static_cast<LPCTSTR>(tooltip), sizeof(pTTTA->szText));
    else
        _mbstowcsz(pTTTW->szText, static_cast<LPCTSTR>(tooltip), 
				sizeof(pTTTW->szText)/sizeof(WCHAR));
#else
    if (pNMHDR->code == TTN_NEEDTEXTA)
        _wcstombsz(pTTTA->szText, static_cast<LPCTSTR>(tooltip), sizeof(pTTTA->szText));
    else
        lstrcpyn(pTTTW->szText, static_cast<LPCTSTR>(tooltip), 
				sizeof(pTTTW->szText)/sizeof(WCHAR));
#endif
    // If wanting to display a tooltip which is longer than 80 characters,
    // one must allocate the needed text-buffer instead of using szText,
    // and point the TOOLTIPTEXT::lpszText to this text-buffer.
    // When doing this, one is required to release this text-buffer again
    return TRUE;
}

When using CWnd::EnableToolTips(), one is sharing the same CToolTipCtrl with all other windows in the application. If we want to modify the behavior of the global CToolTipCtrl, we can acquire it using AfxGetModuleThreadState().

BOOL CListCtrl_EnableToolTip::OnToolNeedText(UINT id, NMHDR* pNMHDR, LRESULT* pResult)
{
...
   // Break tooltip into multiple lines if it contains newlines (/n/r)
   CToolTipCtrl* pToolTip = AfxGetModuleThreadState()->m_pToolTip;
   if (pToolTip)
      pToolTip->SetMaxTipWidth(SHRT_MAX);
...
}

The CToolTipCtrl Member Variable

This method usually requires four steps:

  1. Add the CToolTipCtrl as a member variable, and call CToolTipCtrl::Create() during the control initialization, which for the CListCtrl means over-defining CWnd::PreSubclassWindow().
  2. Over-define CWnd::PreTranslateMessage() for the CListCtrl, and use this method to relay all events to the CToolTipCtrl.
  3. Add a message handler for the mouse move (ON_WM_MOUSEMOVE). This should be used to tell the CToolTipCtrl whether it should display a tool tip.
  4. Implement a TTN_NEEDTEXT message handler. It will be called when the mouse is over the control and the previous method has returned that a tooltip is available.

The last step is only required if supplying the tooltip text using LPSTR_TEXTCALLBACK. If supplying the tooltip text directly (enables tooltip text beyond 80 characters), then we must be aware that we are responsible for any needed deallocation (will not happen automatically). The TTN_NEEDTEXT message handler is identical to the one described in the above paragraph, CWnd::EnableToolTips().

The CToolTipCtrl is mainly intended to be used in CView and CDialog classes, so when adding a tooltip for a control using CToolTipCtrl::AddTool(), the TTN_NEEDTEXT message is sent to the parent. Since we want the CListCtrl to receive the tooltip callback events, we have to create a custom TTM_ADDTOOL message. This is not an issue if providing the tooltip text directly in the mouse move handler.

BEGIN_MESSAGE_MAP(CListCtrl_OwnToolTipCtrl, CListCtrl)
    ON_WM_MOUSEMOVE()
    ON_NOTIFY_EX(TTN_NEEDTEXTA, 0, OnToolNeedText)
    ON_NOTIFY_EX(TTN_NEEDTEXTW, 0, OnToolNeedText)
END_MESSAGE_MAP()

void CListCtrl_OwnToolTipCtrl::PreSubclassWindow()
{
    CListCtrl::PreSubclassWindow();

    // Disable the CToolTipCtrl of CListCtrl so it won't disturb our own tooltip-ctrl
    GetToolTips()->Activate(FALSE);

    // Enable our own tooltip-ctrl and make it show tooltip even if not having focus
    VERIFY( m_OwnToolTipCtrl.Create(this, TTS_ALWAYSTIP) );
    m_OwnToolTipCtrl.Activate(TRUE);
}

BOOL CListCtrl_OwnToolTipCtrl::PreTranslateMessage(MSG* pMsg)
{
    if (m_OwnToolTipCtrl.m_hWnd)
        m_OwnToolTipCtrl.RelayEvent(pMsg);
    return CListCtrl::PreTranslateMessage(pMsg);
}

void CListCtrl_OwnToolTipCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
    CPoint pt(GetMessagePos());
    ScreenToClient(&pt);

    // Find the subitem
    LVHITTESTINFO hitinfo = {0};
    hitinfo.flags = nFlags;
    hitinfo.pt = pt;
    SubItemHitTest(&hitinfo);

    if (m_LastToolTipCol!=hitinfo.iSubItem || m_LastToolTipRow!=hitinfo.iItem)
    {
        // Mouse moved over a new cell
        m_LastToolTipCol = hitinfo.iSubItem;
        m_LastToolTipRow = hitinfo.iItem;

        // Remove the old tooltip (if available)
        if (m_OwnToolTipCtrl.GetToolCount()>0)
        {
            m_OwnToolTipCtrl.DelTool(this);
            m_OwnToolTipCtrl.Activate(FALSE);
        }

        // Add the new tooltip (if available)
        if (m_LastToolTipRow!=-1 && m_LastToolTipRow!=-1)
        {
            // Not using CToolTipCtrl::AddTool() because
            // it redirects the messages to CListCtrl parent
            TOOLINFO ti = {0};
            ti.cbSize = sizeof(TOOLINFO);
            ti.uFlags = TTF_IDISHWND;    // Indicate that uId is handle to a control
            ti.uId = (UINT_PTR)m_hWnd;   // Handle to the control
            ti.hwnd = m_hWnd;            // Handle to window
                                         // to receive the tooltip-messages
            ti.hinst = AfxGetInstanceHandle();
            ti.lpszText = LPSTR_TEXTCALLBACK;
            m_OwnToolTipCtrl.SendMessage(TTM_ADDTOOL, 0, (LPARAM) (LPTOOLINFO) &ti);
            m_OwnToolTipCtrl.Activate(TRUE);
        }
    }

    CListCtrl::OnMouseMove(nFlags, point);
}

CListCtrl::GetToolTips()

This solution only requires that we create a TTN_NEEDTEXT message handler, which is identical to the one described in the above paragraph, CWnd::EnableToolTips.

A few quirks have been discovered with this solution:

  • Subclassing the CToolTipCtrl to perform custom drawing will fail when trying to reposition the tooltip.
  • Tooltips will only show shortly, when running on Windows Vista without Vista-style.
  • Tooltips sometimes get stuck, when running on Windows Vista. When moving the mouse to different cells, the tooltip will stay in the same position, but the tooltip text will be updated correctly.

Extended Style Label Tip

This solution only requires that we add the extended style LVS_EX_LABELTIP. It will only display the tooltip if the cell text cannot be shown completely and the tooltip text can only be the entire cell text.

void CListCtrl_LabelTip::PreSubclassWindow()
{
    CListCtrl::PreSubclassWindow();
    SetExtendedStyle(LVS_EX_INFOTIP | GetExtendedStyle());
}

Extended Style Info Tip

The Info Tip will only work for the label-column, and it is limited to 80 characters. The solution requires two steps:

  1. Enable the extended style LVS_EX_INFOTIP during the control initialization, which for the CListCtrl means over-defining CWnd::PreSubclassWindow().
  2. Implement a LVN_GETINFOTIP message handler. It will be called when the mouse is over a cell in the label-column.
BEGIN_MESSAGE_MAP(CListCtrl_InfoTip, CListCtrl)
    ON_NOTIFY_REFLECT_EX(LVN_GETINFOTIP, OnGetInfoTip)
END_MESSAGE_MAP()

void CListCtrl_InfoTip::PreSubclassWindow()
{
    CListCtrl::PreSubclassWindow();
    SetExtendedStyle(LVS_EX_INFOTIP | GetExtendedStyle());
}

BOOL CListCtrl_InfoTip::OnGetInfoTip(NMHDR* pNMHDR, LRESULT* pResult)
{
    // Will only request tooltip for the label-column
    NMLVGETINFOTIP* pInfoTip = (NMLVGETINFOTIP*)pNMHDR;
    CString tooltip = GetToolTipText(pInfoTip->iItem, pInfoTip->iSubItem);
    if (!tooltip.IsEmpty())
    {
        _tcsncpy(pInfoTip->pszText, static_cast<LPCTSTR>(tooltip), pInfoTip->cchTextMax);
    }
    return FALSE;    // Let parent-dialog get chance
}

Using the Code

The source code provides examples of how to implement the described tooltip solutions for CListCtrl.

History

  • 2008-08-28 - First release of the article
  • 2009-03-07 - Fixed unicode bugs

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here