Tooltip for individual cells

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >


Tooltips are very useful when column widths are limited due to limited screen size. They could also be used to expand the text of abreviated columns. For this task, we will use the tooltip support provided by MFC. The code below displays the text of the cell in a tooltip, but it can be easily modified to display something other that whats already displayed in the cell.

Adding tooltips for individual cells is quite easy. However, the documentation was not very helpful and there are few details that you should be aware of. The list view control on Windows 95 and Windows NT 4.0 have two very significant differences. First, the list view control ( and the tooltip control ) is an ANSI control on Windows 95. What this means is that on Windows 95, the control notifications and messages are the ANSI versions. Do not rely on the project setting to automatically translate the plain, undecorated form of a message constant (e.i. without the A or W suffix) to the correct message value. For example, if you were developing a UNICODE application, then TTN_NEEDTEXT would translate to TTN_NEEDTEXTW, but on Windows 95 the actual message would be TTN_NEEDTEXTA. This also applies to structures and strings. On Window 95, all strings passed to a control should be ANSI strings. On NT 4.0, the controls are UNICODE controls.

Second, on NT 4.0, the list view control automatically creates a tooltip control. This built in tooltip control automatically sends the TTN_NEEDTEXTW notification whenever the mouse is over the list view control and does not move for a certian duration. The code below ignores notification from this built in tooltip control.
 

Override PreSubclassWindow() and after calling the base class version call EnableToolTips(TRUE). EnableToolTips() is a member function of the CWnd class and thus available to all windows and controls.
void CMyListCtrl::PreSubclassWindow() 
{
	CListCtrl::PreSubclassWindow();

	// Add initialization code
	EnableToolTips(TRUE);
}
Override the OnToolHitTest() function. OnToolHitTest() is a virtual function defined in CWnd class and is called by the framework to determine whether a point is over any of the tools. A tool can be a control window or even a rectangular area within a window. For our purpose, we would want the area of each cell to be treated as a tool.

The documentation of the OnToolHitTest() implies that you should return 1 if a tool is found and -1 if no tool was found. The truth, however, is that, the framework uses the return value to determine if the tool has changed. The framework updates the tooltip only when the tool changes, therefore, OnToolHitTest() should return a different value whenever the cell at the given point changes.

int CMyListCtrl::OnToolHitTest(CPoint point, TOOLINFO * pTI) const
{
	int row, col;
	RECT cellrect;
	row = CellRectFromPoint(point, &cellrect, &col );

	if ( row == -1 ) 
		return -1;

	pTI->hwnd = m_hWnd;
	pTI->uId = (UINT)((row<<10)+(col&0x3ff)+1);
	pTI->lpszText = LPSTR_TEXTCALLBACK;

	pTI->rect = cellrect;

	return pTI->uId;
}
The function first calls the CellRectFromPoint() function to determine the row, column and the bounding rectangle of the cell. We cover the CellRectFromPoint() below. The function then sets up the TOOLINFO structure. The uId is assigned a value by combining the row and col values. Our method of combining the row and column values will allow upto 4194303 rows and 1023 columns. Also, note that a 1 is added to the result. The reason for this is to make this a non zero value. We need a non zero id so that we can distinguish it from the notification sent from the automatically created tooltip on NT 4.0. As mentioned earlier, on NT 4.0, the list view control automatically creates a tooltip, and the id used by this tooltip is 0.

We next define the CellRectFromPoint() function that is used by OnToolHitTest(). This function is very similar to the HitTextEx() function covered in an earlier topic. In addition to determining the row and column over which a point falls, this function also determines the bounding rectangle of the cell thats under the point.

// CellRectFromPoint	- Determine the row, col and bounding rect of a cell
// Returns		- row index on success, -1 otherwise
// point		- point to be tested.
// cellrect		- to hold the bounding rect
// col			- to hold the column index
int CMyListCtrl::CellRectFromPoint(CPoint & point, RECT * cellrect, int * col) const
{
	int colnum;

	// Make sure that the ListView is in LVS_REPORT
	if( (GetWindowLong(m_hWnd, GWL_STYLE) & LVS_TYPEMASK) != LVS_REPORT )
		return -1;

	// Get the top and bottom row visible
	int row = GetTopIndex();
	int bottom = row + GetCountPerPage();
	if( bottom > GetItemCount() )
		bottom = GetItemCount();
	
	// Get the number of columns
	CHeaderCtrl* pHeader = (CHeaderCtrl*)GetDlgItem(0);
	int nColumnCount = pHeader->GetItemCount();

	// Loop through the visible rows
	for( ;row <=bottom;row++)
	{
		// Get bounding rect of item and check whether point falls in it.
		CRect rect;
		GetItemRect( row, &rect, LVIR_BOUNDS );
		if( rect.PtInRect(point) )
		{
			// Now find the column
			for( colnum = 0; colnum < nColumnCount; colnum++ )
			{
				int colwidth = GetColumnWidth(colnum);
				if( point.x >= rect.left 
					&& point.x <= (rect.left + colwidth ) )
				{
					RECT rectClient;
					GetClientRect( &rectClient );
					if( col ) *col = colnum;
					rect.right = rect.left + colwidth;

					// Make sure that the right extent does not exceed
					// the client area
					if( rect.right > rectClient.right ) 
						rect.right = rectClient.right;
					*cellrect = rect;
					return row;
				}
				rect.left += colwidth;
			}
		}
	}
	return -1;
}
Define OnToolTipText(). This is the handler for the TTN_NEEDTEXT notification from the tooltip. Actually, OnToolTipText() handles both the TTN_NEEDTEXTA and TTN_NEEDTEXTW notifications and uses ANSI strings for the former notification and UNICODE strings for the latter irrespective of whether the application itself is ANSI or UNICODE.
BOOL CMyListCtrl::OnToolTipText( UINT id, NMHDR * pNMHDR, LRESULT * pResult )
{
	// need to handle both ANSI and UNICODE versions of the message
	TOOLTIPTEXTA* pTTTA = (TOOLTIPTEXTA*)pNMHDR;
	TOOLTIPTEXTW* pTTTW = (TOOLTIPTEXTW*)pNMHDR;
	CString strTipText;
	UINT nID = pNMHDR->idFrom;

	if( nID == 0 )	  	// Notification in NT from automatically
		return FALSE;   	// created tooltip

	int row = ((nID-1) >> 10) & 0x3fffff ;
	int col = (nID-1) & 0x3ff;
	strTipText = GetItemText( row, col );

#ifndef _UNICODE
	if (pNMHDR->code == TTN_NEEDTEXTA)
		lstrcpyn(pTTTA->szText, strTipText, 80);
	else
		_mbstowcsz(pTTTW->szText, strTipText, 80);
#else
	if (pNMHDR->code == TTN_NEEDTEXTA)
		_wcstombsz(pTTTA->szText, strTipText, 80);
	else
		lstrcpyn(pTTTW->szText, strTipText, 80);
#endif
	*pResult = 0;

	return TRUE;    // message was handled
}
The function first checks whether the notification is from the built in tooltip (on NT only) and returns immediately if it is. The function then decodes the row and column information from the id and then sets up the TOOLTIPTEXT structure with the text in the cell.
 

Hook up OnToolTipText() in the message map. Its a good idea to use the ON_NOTIFY_EX and ON_NOTIFY_EX_RANGE macros, since this allows the notification to be propogated for further message processing if needed.

BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl)
	//{{AFX_MSG_MAP(CMyListCtrl)
	:
	// other entries
	:
	//}}AFX_MSG_MAP
	ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipText)
	ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipText)
END_MESSAGE_MAP()
Note that we are not using a simple ON_NOTIFY macro. Infact if you were using a message map entry such as
ON_NOTIFY(TTN_NEEDTEXT, 0, OnToolTipText)

you would have some major problems. First, the TTN_NEEDTEXT define would translate to TTN_NEEDTEXTA on an ANSI build and this notification is never received on NT 4. Second, we are indicating that we are only interested in the id 0, which is not usually the case.
 
 



Comments

  • Thank you

    Posted by Legacy on 11/21/2003 12:00am

    Originally posted by: Bob Rader

    I took your code for my MS VS6.0 project and it worked fine.

    Reply
  • Much easier way with LVS_EX_INFOTIP

    Posted by Legacy on 07/28/2002 12:00am

    Originally posted by: Alon Oren

    Use:
    ListView_SetExtendedListViewStyle(ListCtrl.m_hWnd,LVS_EX_INFOTIP);
    (if you have IE4 or higher installed).

    or read http://www.codeguru.com/listview/ie4_extended_styles_to_a_list_control.shtml

    • This method you suggest only works on the first column.

      Posted by Sprintstar on 11/15/2006 07:18am

      Which is why I guess this article was written in the first place!

      Reply
    Reply
  • Thanks

    Posted by Legacy on 06/28/2002 12:00am

    Originally posted by: Claude STRUB

    Thanks,
    with the patch for ActiveX from Andreas Benamou, it works Fine !

    Reply
  • Fix for LVS_EX_HEADERDRAGDROP style enabled

    Posted by Legacy on 11/08/2001 12:00am

    Originally posted by: Luis G

    The code works greate so far until you reorder the columns.  For example, colnum 3 really could be column 1 in a different order.  Here is a trivial fix for this problem:
    
    

    int CGtListView::CellRectFromPoint(CPoint & point, RECT * cellrect, int * col) const
    {
    ...

    // Get bounding rect of item and check whether point falls in it.
    CRect rect;
    GetListCtrl().GetItemRect( row, &rect, LVIR_BOUNDS );
    if( rect.PtInRect(point) )
    {
    // Now find the column
    for( colnum = 0; colnum < nColumnCount; colnum++ )
    {
    // Make sure we are looking at the right column if it is in a different order
    int colindex = GetListCtrl().GetHeaderCtrl()->OrderToIndex(colnum);

    // Get column width
    int colwidth = GetListCtrl().GetColumnWidth( colindex );

    if( point.x >= rect.left
    && point.x <= (rect.left + colwidth ) )
    {
    RECT rectClient;
    GetClientRect( &rectClient );

    if( col )
    {
    *col = colindex;
    }
    ...
    }

    Reply
  • Fix for use with tooltips in toolbar

    Posted by Legacy on 10/23/2001 12:00am

    Originally posted by: Andy [FineClayMatrix]

    As the code currently stands the tooltips that appear for the standard buttons in the toolbar dont appear, and custom toolbar buttons show tooltips relating to the list view.  To get around this (tested in Win 98 only) replace:
    
    

    if(nID == 0) // Notification in NT from automatically
    return FALSE; // created tooltip


    with the following:


    if(nID == 0) // Notification in NT from automatically
    return TRUE; // created tooltip

    //Get the cursor position
    CPoint posMouse;
    GetCursorPos(&posMouse);

    //Check that it is over the list view
    CRect rcLView;
    GetWindowRect(&rcLView);
    if(!rcLView.PtInRect(posMouse)) return FALSE;


    The extra code checks that the pointer is actually over the the list view window and not elsewhere in the application (like over the toolbar buttons).

    Andy

    Reply
  • Does not work when you have a toolbar ctrl.

    Posted by Legacy on 10/18/2001 12:00am

    Originally posted by: Russell Bonakdar

    My app contains a listview control and a toolbar ctrl with toolbar buttons. The toolbar button tooltips do not get displayed because the OnToolTipText() does not take them into account (nID!=0) Any one have any ideas?

    Thanks.

    Reply
  • Tooltips in CListCtrl: Maybe this will help someone...

    Posted by Legacy on 08/06/2001 12:00am

    Originally posted by: Justin Crowell

    I've also been having problems with CListCtrl and tooltips in NT4. So here are some things I've noticed, maybe they will help someone:
    
    

    For some reason my over-ride for OnToolHitTest() wasn't ever being called when tooltips have been enabled for the CListCtrl. Then I noticed I didn't have a "const" at the end of the function, so then I tried it:

    OnToolHitTest(CPoint point, TOOLINFO* pTI) const

    So now finally it's hitting my breakpoint. Since this method is an over-ride of an existing method, it needs to be defined exactly the same (that const is pretty easy to miss).

    After I got that working I tried something like this in OnToolHitTest():

    int index = HitTest(point);
    if (index != -1)
    {
    pTI->hwnd = GetParent()->m_hWnd;
    pTI->uId = (UINT)m_hWnd;
    pTI->uFlags |= TTF_IDISHWND;
    pTI->lpszText = LPSTR_TEXTCALLBACK;
    return 1;
    }
    else return -1;

    Now I stepped through the code so I saw that it was succeeding and returning 1, but my handler for TTN_NEEDTEXT in the parent dialog was never getting called. So then I tried changing this:

    pTI->lpszText = LPSTR_TEXTCALLBACK;

    To this:

    pTI->lpszText = "Test";

    Which then gave me an access violation with the debugger pointing at a call to free() somewhere in the MFC code. So then I tried replacing that line with this:

    char *Test = (char *)malloc(10);
    lstrcpy(Test, "Test");
    pTI->lpszText = Test;

    And finally, I saw my test tooltip, and MFC freed the memory. I had to use malloc() to match the free() call in MFC. So I figured I could just generate the tooltip in here and not mess with handling TTN_NEEDTEXT. So then, needing to format the tooltip with data depending on the item it's over, I changed the method to this:

    // Get index to item mouse is over
    int index = HitTest(point);
    if (index != -1)
    {
    // Setup tooltip structure
    pTI->hwnd = GetParent()->m_hWnd;
    pTI->uId = (UINT)m_hWnd;
    pTI->uFlags |= TTF_IDISHWND;

    // Format tooltip
    CString ToolTip;
    ToolTip.Format("... Item Data Here ...");

    // Allocate memory where we will store tooltip (MFC will free it)
    pTI->lpszText = (char *)malloc(ToolTip.GetLength()+1);
    lstrcpy(pTI->lpszText, ToolTip.GetBuffer(0));
    return 1;
    }
    else return -1;

    So that worked and my formatted tip appeared, but then I noticed that it stays there and remains the same when I move the mouse between items. So, remembering reading something from one of the guys on this page earlier about returning different values to update the tip, I changed the line:

    return 1;

    To:

    return index;

    So now it's working perfectly. Note that you could change the HitTest() call to SubItemHitTest() to get the subitem index for the mouse position also.

    I'm not used to dealing with lame Windows poblems like this since I usually use Borland C++ Builder. But hopefully this will help somebody out.

    -Justin

    Reply
  • Works in 98 but 2000 doesn't work right with VC 6.0

    Posted by Legacy on 07/20/2001 12:00am

    Originally posted by: Mark eby

    This code works fine in Windows 98 to display what I want in the tool tip.

    In Windows 2000 all I get is an occasiosional flicker and I had a terrible time getting the <EnableToolTips(TRUE);> line it because I couldn't find a function that was called after the CWnd class is instantiated but before the window is displayed in a dialog.

    This seems to be the same basic problem tbrown has and I did include the virtual keyword but the function is not called unless I click on the item.

    It would be nice if LVN_GETINFOTIP would work as advertised when IE4.0 or later is installed.

    I really need a reliable cell based tooltip that I can control the contents in (always) in any version of windows with IE4 or greater installed.

    Reply
  • Thanks! Thanks!...

    Posted by Legacy on 05/18/2001 12:00am

    Originally posted by: JeongHwan Cho

    Thank you so much!
    Very good code!

    Reply
  • Great, also under W2K

    Posted by Legacy on 04/26/2001 12:00am

    Originally posted by: Peter Schmidt

    Really great feature! Also the code and comments are very good. On my computer it also works under Win2K, perhaps tbrown forgot the virtual keyword?

    Thanks a lot!

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The software-defined data center (SDDC) and new trends in cloud and virtualization bring increased agility, automation, and intelligent services and management to all areas of the data center. Businesses can now more easily manage the entire lifecycle of their applications and services via the SDDC. This Aberdeen analyst report examines how a strong foundation in both the cloud and internal data centers is empowering organizations to fully leverage their IT infrastructure and is also preparing them to be able …

  • Analytics in the cloud democratize the ability of developers and non-technical business users alike to put ever-expanding data universes to work. Download this eBook to learn how to plan and build a cloud-based enterprise analytics platform, tune that environment to meet changing business requirements, and maximize the TCO benefits of moving analytics to the cloud.

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date