CListCtrl 和显示工具提示






4.89/5 (33投票s)
在 MFC 列表控件中实现工具提示的示例
引言
Microsoft 的 CListCtrl
支持在网格中显示数据,但需要一些帮助才能显示工具提示。本文将演示如何在使用 CListCtrl
时显示工具提示。演示应用程序允许您体验不同工具提示实现的行为。

背景
有很多高级网格控件扩展了 CListCtrl
,因此可以在将鼠标悬停在单元格上时显示工具提示。但是,由于这些网格控件可能非常复杂,因此很难看出它们是如何实现的。
如何在 CListCtrl 中实现工具提示
对于普通的 MFC 控件,通常有两种启用工具提示的方法:
- 调用
CWnd::EnableToolTips()
- 添加
CToolTipCtrl
作为成员变量
CListCtrl
还有一些其他技巧,可以提供更多选项:
- 拥有自己的
CToolTipCtrl
成员变量,可以使用CListCtrl::GetToolTips()
访问。 - 识别扩展样式
LVS_EX_LABELTIP
,当鼠标悬停在文本未完全显示的单元格上时,该样式会激活工具提示。 - 识别扩展样式
LVS_EX_INFOTIP
,该样式为标签列启用工具提示。
本文将仅专注于如何为 CListCtrl
中的单元格显示简单的工具提示。实现工具提示是一个相当大的主题,可以在 MSDN 文章中找到:关于工具提示控件 和 使用工具提示控件。
CWnd::EnableToolTips()
此方法通常需要三个步骤:
- 在控件初始化期间调用
CWnd::EnableToolTips(TRUE)
,对于CListCtrl
而言,这意味着重写CWnd::PreSubclassWindow()
。 - 为
CListCtrl
重写CWnd::OnToolHitTest(...)
。每次鼠标移动到控件上时都会调用它,函数的返回值指定是否应显示工具提示。 - 实现
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 成员变量
此方法通常需要四个步骤:
- 将
CToolTipCtrl
添加为成员变量,并在控件初始化期间调用CToolTipCtrl::Create()
,对于CListCtrl
而言,这意味着重写CWnd::PreSubclassWindow()
。 - 为
CListCtrl
重写CWnd::PreTranslateMessage()
,并使用此方法将所有事件转发给CToolTipCtrl
。 - 添加鼠标移动的消息处理程序(
ON_WM_MOUSEMOVE
)。这应该用于告知CToolTipCtrl
是否应显示工具提示。 - 实现
TTN_NEEDTEXT
消息处理程序。当鼠标位于控件上并且前一个方法返回工具提示可用时,将调用它。
只有在通过 LPSTR_TEXTCALLBACK
提供工具提示文本时,才需要最后一步。如果直接提供工具提示文本(允许工具提示文本超过 80 个字符),那么我们必须注意,我们负责任何必要的释放(不会自动发生)。TTN_NEEDTEXT
消息处理程序与上面段落中描述的 CWnd::EnableToolTips()
相同。
CToolTipCtrl
主要用于 CView
和 CDialog
类,因此在通过 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 个字符。该解决方案需要两个步骤:
- 在控件初始化期间启用扩展样式
LVS_EX_INFOTIP
,对于CListCtrl
而言,这意味着重写CWnd::PreSubclassWindow()
。 - 实现
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 错误