65.9K
CodeProject 正在变化。 阅读更多。
Home

CListCtrl 和显示工具提示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (33投票s)

2008年8月27日

CPOL

5分钟阅读

viewsIcon

168163

downloadIcon

8914

在 MFC 列表控件中实现工具提示的示例

引言

Microsoft 的 CListCtrl 支持在网格中显示数据,但需要一些帮助才能显示工具提示。本文将演示如何在使用 CListCtrl 时显示工具提示。演示应用程序允许您体验不同工具提示实现的行为。

screenshot.png

背景

有很多高级网格控件扩展了 CListCtrl,因此可以在将鼠标悬停在单元格上时显示工具提示。但是,由于这些网格控件可能非常复杂,因此很难看出它们是如何实现的。

如何在 CListCtrl 中实现工具提示

对于普通的 MFC 控件,通常有两种启用工具提示的方法:

  • 调用 CWnd::EnableToolTips()
  • 添加 CToolTipCtrl 作为成员变量

CListCtrl 还有一些其他技巧,可以提供更多选项:

  • 拥有自己的 CToolTipCtrl 成员变量,可以使用 CListCtrl::GetToolTips() 访问。
  • 识别扩展样式 LVS_EX_LABELTIP,当鼠标悬停在文本未完全显示的单元格上时,该样式会激活工具提示。
  • 识别扩展样式 LVS_EX_INFOTIP,该样式为标签列启用工具提示。

本文将仅专注于如何为 CListCtrl 中的单元格显示简单的工具提示。实现工具提示是一个相当大的主题,可以在 MSDN 文章中找到:关于工具提示控件使用工具提示控件

CWnd::EnableToolTips()

此方法通常需要三个步骤:

  1. 在控件初始化期间调用 CWnd::EnableToolTips(TRUE),对于 CListCtrl 而言,这意味着重写 CWnd::PreSubclassWindow()
  2. CListCtrl 重写 CWnd::OnToolHitTest(...)。每次鼠标移动到控件上时都会调用它,函数的返回值指定是否应显示工具提示。
  3. 实现 TTN_NEEDTEXT 消息处理程序。当鼠标位于控件上并且前一个方法返回工具提示可用时,将调用它。

仅当第二个步骤指定应通过回调(LPSTR_TEXTCALLBACK)检索工具提示文本时,第三个步骤才是必需的。执行回调的原因仅是为了速度考虑,以防工具提示文本的查找速度很慢。而不是指定回调,我们可以直接返回工具提示文本,通过执行 malloc() 来保存文本(它将自动释放)并允许工具提示文本超过 80 个字符。

如果使用 TTN_NEEDTEXT 消息处理程序并希望显示超过 80 个字符的工具提示,则必须分配所需的文本缓冲区,并在消息处理程序中将 TOOLTIPTEXT::lpszText 指针设置为此文本缓冲区(必须手动释放此文本缓冲区)。

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;
}

使用 CWnd::EnableToolTips() 时,您将与应用程序中的所有其他窗口共享同一个 CToolTipCtrl。如果我们想修改全局 CToolTipCtrl 的行为,可以使用 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);
...
}

CToolTipCtrl 成员变量

此方法通常需要四个步骤:

  1. CToolTipCtrl 添加为成员变量,并在控件初始化期间调用 CToolTipCtrl::Create(),对于 CListCtrl 而言,这意味着重写 CWnd::PreSubclassWindow()
  2. CListCtrl 重写 CWnd::PreTranslateMessage(),并使用此方法将所有事件转发给 CToolTipCtrl
  3. 添加鼠标移动的消息处理程序(ON_WM_MOUSEMOVE)。这应该用于告知 CToolTipCtrl 是否应显示工具提示。
  4. 实现 TTN_NEEDTEXT 消息处理程序。当鼠标位于控件上并且前一个方法返回工具提示可用时,将调用它。

只有在通过 LPSTR_TEXTCALLBACK 提供工具提示文本时,才需要最后一步。如果直接提供工具提示文本(允许工具提示文本超过 80 个字符),那么我们必须注意,我们负责任何必要的释放(不会自动发生)。TTN_NEEDTEXT 消息处理程序与上面段落中描述的 CWnd::EnableToolTips() 相同。

CToolTipCtrl 主要用于 CViewCDialog 类,因此在通过 CToolTipCtrl::AddTool() 为控件添加工具提示时,TTN_NEEDTEXT 消息会发送到父窗口。由于我们希望 CListCtrl 接收工具提示回调事件,因此我们必须创建一个自定义的 TTM_ADDTOOL 消息。如果直接在鼠标移动处理程序中提供工具提示文本,则这不是问题。

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()

此解决方案仅要求我们创建一个 TTN_NEEDTEXT 消息处理程序,它与上面段落中描述的 CWnd::EnableToolTips 相同。

此解决方案已发现一些小问题:

  • 在尝试重新定位工具提示时,将 CToolTipCtrl 子类化以执行自定义绘制将失败。
  • 在 Windows Vista 上不带 Vista 样式运行时,工具提示只会短暂显示。
  • 在 Windows Vista 上运行时,工具提示有时会卡住。将鼠标移动到不同单元格时,工具提示会保持在同一位置,但工具提示文本会正确更新。

扩展样式标签提示

此解决方案仅要求我们添加扩展样式 LVS_EX_LABELTIP。只有当单元格文本无法完全显示时,并且工具提示文本只能是整个单元格文本时,它才会显示工具提示。

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

扩展样式信息提示

信息提示仅对标签列有效,并且限制为 80 个字符。该解决方案需要两个步骤:

  1. 在控件初始化期间启用扩展样式 LVS_EX_INFOTIP,对于 CListCtrl 而言,这意味着重写 CWnd::PreSubclassWindow()
  2. 实现 LVN_GETINFOTIP 消息处理程序。当鼠标位于标签列的单元格上时,将调用它。
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

源代码提供了有关如何为 CListCtrl 实现所述工具提示解决方案的示例。

历史

  • 2008-08-28 - 文章首次发布
  • 2009-03-07 - 修复了 Unicode 错误
© . All rights reserved.