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.
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:
- Call
CWnd::EnableToolTips(TRUE)
during the control initialization, which for the CListCtrl
means over-defining CWnd::PreSubclassWindow()
.
- 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.
- 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();
GetToolTips()->Activate(FALSE);
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);
RECT rcClient;
GetClientRect( &rcClient );
pTI->hwnd = m_hWnd;
pTI->uId = (UINT) (nRow * 1000 + nCol);
pTI->lpszText = LPSTR_TEXTCALLBACK;
pTI->rect = rcClient;
return pTI->uId;
}
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;
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
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)
{
...
CToolTipCtrl* pToolTip = AfxGetModuleThreadState()->m_pToolTip;
if (pToolTip)
pToolTip->SetMaxTipWidth(SHRT_MAX);
...
}
The CToolTipCtrl Member Variable
This method usually requires four steps:
- Add the
CToolTipCtrl
as a member variable, and call CToolTipCtrl::Create()
during the control initialization, which for the CListCtrl
means over-defining CWnd::PreSubclassWindow()
.
- Over-define
CWnd::PreTranslateMessage()
for the CListCtrl
, and use this method to relay all events to the CToolTipCtrl
.
- 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.
- 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();
GetToolTips()->Activate(FALSE);
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);
LVHITTESTINFO hitinfo = {0};
hitinfo.flags = nFlags;
hitinfo.pt = pt;
SubItemHitTest(&hitinfo);
if (m_LastToolTipCol!=hitinfo.iSubItem || m_LastToolTipRow!=hitinfo.iItem)
{
m_LastToolTipCol = hitinfo.iSubItem;
m_LastToolTipRow = hitinfo.iItem;
if (m_OwnToolTipCtrl.GetToolCount()>0)
{
m_OwnToolTipCtrl.DelTool(this);
m_OwnToolTipCtrl.Activate(FALSE);
}
if (m_LastToolTipRow!=-1 && m_LastToolTipRow!=-1)
{
TOOLINFO ti = {0};
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_IDISHWND;
ti.uId = (UINT_PTR)m_hWnd;
ti.hwnd = m_hWnd;
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:
- Enable the extended style
LVS_EX_INFOTIP
during the control initialization, which for the CListCtrl
means over-defining CWnd::PreSubclassWindow()
.
- 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)
{
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;
}
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