Click here to Skip to main content
14,240,573 members
Rate this:
Please Sign up or sign in to vote.
See more:
I have subclassed edit control to accept only floating numbers. I would like to pop a tooltip when user makes an invalid input. The behavior I target is like the one edit control with ES_NUMBER has, please see this[^].

So far I was able to implement tracking tooltip and display it when user makes invalid input.

However, the tooltip is misplaced. I have tried to use ScreenToClient and ClientToScreen to fix this but have failed.

Here are the instructions for creating SCCE :

1) Create default Win32 project in Visual Studio.

2) Add the following includes in your stdafx.h, just under #include <windows.h> :

#include <windowsx.h>
#include <commctrl.h>

#pragma comment( lib, "comctl32.lib")

#pragma comment(linker, \
    "\"/manifestdependency:type='Win32' "\
    "name='Microsoft.Windows.Common-Controls' "\
    "version='6.0.0.0' "\
    "processorArchitecture='*' "\
    "publicKeyToken='6595b64144ccf1df' "\
    "language='*'\"")


3) Add these global variables:

HWND g_hwndTT;<br />
TOOLINFO g_ti;


4) Here is a simple subclass procedure for edit controls ( just for testing purposes ) :

LRESULT CALLBACK EditSubProc ( HWND hwnd, UINT message, 
    WPARAM wParam, LPARAM lParam, 
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    switch (message)
    {
    case WM_CHAR:
        {
            POINT pt;
            if( ! isdigit( wParam ) )  // if not a number pop a tooltip!
            {
                if (GetCaretPos(&pt))  // here comes the problem
                {
                    // coordinates are not good, so tooltip is misplaced
                    ClientToScreen( hwnd, &pt );
                    
                    /************************* EDIT : ****************************/
                    /******** After I delete this line x-coordinate is OK ********/
                    /*** y-coordinate could be a little lower, but is still OK ***/
                    /*************************************************************/
                    ScreenToClient( GetParent(hwnd), &pt );

                    SendMessage(g_hwndTT, TTM_TRACKACTIVATE, 
                        TRUE, (LPARAM)&g_ti);
                    SendMessage(g_hwndTT, TTM_TRACKPOSITION, 
                        0, MAKELPARAM(pt.x, pt.y));
                }
                return FALSE;
            }
            else
            {
                SendMessage(g_hwndTT, TTM_TRACKACTIVATE, 
                    FALSE, (LPARAM)&g_ti);
                return ::DefSubclassProc( hwnd, message, wParam, lParam );
            }
        }
        break;
    case WM_NCDESTROY:
        ::RemoveWindowSubclass( hwnd, EditSubProc, 0 );
        return DefSubclassProc( hwnd, message, wParam, lParam);
        break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
} 


5) Add the following WM_CREATE handler :

case WM_CREATE:
    {
        HWND hEdit = CreateWindowEx( 0, L"EDIT", L"edit", WS_CHILD | WS_VISIBLE |
            WS_BORDER | ES_CENTER, 150, 150, 100, 30, hWnd, (HMENU)1000, hInst, 0 );

        // try with tooltip
        g_hwndTT = CreateWindow(TOOLTIPS_CLASS, NULL,
            WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
            0, 0, 0, 0, hWnd, NULL, hInst, NULL);

        if( !g_hwndTT )
            MessageBeep(0);  // just to signal error somehow

        g_ti.cbSize = sizeof(TOOLINFO);
        g_ti.uFlags = TTF_TRACK | TTF_ABSOLUTE;
        g_ti.hwnd = hWnd;
        g_ti.hinst = hInst;
        g_ti.lpszText = TEXT("Hi there");

        if( ! SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&g_ti) )
            MessageBeep(0);  // just to have some error signal

        // subclass edit control
        SetWindowSubclass( hEdit, EditSubProc, 0, 0 );
    }
    return 0L;  


6) Initialize common controls in MyRegisterClass ( before return statement ) :
// initialize common controls
INITCOMMONCONTROLSEX iccex;
iccex.dwSize = sizeof(INITCOMMONCONTROLSEX);
iccex.dwICC = ICC_BAR_CLASSES | ICC_WIN95_CLASSES | 
    ICC_TAB_CLASSES | ICC_TREEVIEW_CLASSES | ICC_STANDARD_CLASSES ;

if( !InitCommonControlsEx(&iccex) ) 
    MessageBeep(0);   // signal error 


That's it, for the SSCCE.

My questions are following :

1. How can I properly position tooltip in my main window? How should I manipulate with caret coordinates?

2. Is there a way for tooltip handle and toolinfo structure to not be global?

Thank you for your time.

Best regards.

EDIT #1:

I have managed to achieve quite an improvement by deleting ScreenToClient call in the subclass procedure. The x-coordinate is good, y-coordinate could be slightly lower. I still would like to remove global variables somehow...

EDIT #2:

I was able to adjust y-coordinate by using EM_GETRECT[^] message and setting y-coordinate to the bottom of the formatting rectangle:
RECT rcClientRect;
Edit_GetRect( hwnd, &rcClientRect );
pt.y = rcClient.bottom;

Now the end-result is much better. All that is left is to remove global variables...

EDIT #3:

It seems that I have cracked it! The solution is in EM_SHOWBALLOONTIP and EM_HIDEBALLOONTIP messages! Tooltip is placed at the caret position, ballon shape is the same as the one on the picture, and it auto-dismisses itself properly. And the best thing is that I do not need global variables!

Here is my subclass procedure snippet:
case WM_CHAR:
    {
        // whatever... This condition is for testing purpose only
        if( ! IsCharAlpha( wParam ) && IsCharAlphaNumeric( wParam ) )
        {
            SendMessage(hwnd, EM_HIDEBALLOONTIP, 0, 0);
            return ::DefSubclassProc( hwnd, message, wParam, lParam );
        }
        else
        {
            EDITBALLOONTIP ebt;

            ebt.cbStruct = sizeof( EDITBALLOONTIP );
            ebt.pszText = L" Tooltip text! ";
            ebt.pszTitle = L" Tooltip title!!! ";
            ebt.ttiIcon = TTI_ERROR_LARGE;    // tooltip icon
				
            SendMessage(hwnd, EM_SHOWBALLOONTIP, 0, (LPARAM)&ebt);
       
            return FALSE;
        }
     }
     break;
Posted
Updated 29-May-14 1:27am
v6
Rate this:
Please Sign up or sign in to vote.

Solution 4

After further testing, I have decided to put this as an answer so others can clearly spot it.

The solution is in using EM_SHOWBALLOONTIP and EM_HIDEBALLOONTIP messages. You do not need to create tooltip and associate it to an edit control! Therefore, all I need to do now is simply subclass edit control and everything works :

LRESULT CALLBACK EditSubProc ( HWND hwnd, UINT message, 
WPARAM wParam, LPARAM lParam, 
UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    switch (message)
    {
    case WM_CHAR:
        {
            if( ! isdigit( wParam ) )  // if not a number pop a tooltip!
            {
                EDITBALLOONTIP ebt;

                ebt.cbStruct = sizeof( EDITBALLOONTIP );
                ebt.pszText = L" Tooltip text! ";
                ebt.pszTitle = L" Tooltip title!!! ";
                ebt.ttiIcon = TTI_ERROR_LARGE;    // tooltip icon

                SendMessage(hwnd, EM_SHOWBALLOONTIP, 0, (LPARAM)&ebt);
                return FALSE;
            }
            else
            {
                SendMessage(hwnd, EM_HIDEBALLOONTIP, 0, 0);
                return ::DefSubclassProc( hwnd, message, wParam, lParam );
            }
        }
        break;
    case WM_NCDESTROY:
        ::RemoveWindowSubclass( hwnd, EditSubProc, 0 );
        return DefSubclassProc( hwnd, message, wParam, lParam);
        break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
} 

That's it!

Hopefully this answer will help someone too!
   
Rate this:
Please Sign up or sign in to vote.

Solution 3

By the way there was a more interesting part of the number edit tooltip in that it has the cross icon in it and I am currently playing around trying to work out how they did that. I also not clicking on the tooltip dismisses it so got a couple more things to work out.
   
Comments
AlwaysLearningNewStuff 29-May-14 6:50am
   
For the cross icon you must use TTM_SETTITLE, see the docs here : http://msdn.microsoft.com/en-us/library/windows/desktop/bb760414%28v=vs.85%29.aspx

As for tooltip getting dismissed, I think they did not use tracking tooltip at all. There is a message EM_SHOWBALLOONTIP that "displays a balloon tip associated with an edit control", so maybe they adjust rectangle of the tooltip before they use this message. This would remove global variables, and could make tooltip autodismiss itself after certain amount of time.
AlwaysLearningNewStuff 29-May-14 7:24am
   
It seems that I have cracked it! See my third edit.
leon de boer 29-May-14 9:37am
   
That is actually the best solution and it answered the other question I had which was how did they put the icon there and that answers that because you have the icon options in the EDITBALLOONTIP structure under ttiIcon. I voted it a 5 nicely done it's very neat.

It does give me something I am going to have a look at in it's underbelly however which is how they have shaped the window because it clearly doesn't work off a bitmap mask because it changes when it gets close to the edges. So I am guessing it's a poly shape they mathematically create and use much like I made the button in that validate sample. Something to ponder and play with because there may be some more interesting subclass plays to be had here.
AlwaysLearningNewStuff 29-May-14 13:20pm
   
Thank you so much! As for tooltip icon, there is also a message that sets title and icon of a tooltip, please see this message : http://msdn.microsoft.com/en-us/library/windows/desktop/bb760414%28v=vs.85%29.aspx

Notice how you can also put your own icon instead of predefined one.

As for window shape, well I am not that strong with GDI / GDI+ so I am content with the behavior I have currently... Good luck with your experimenting! If you find something interesting I would gladly take a look at the results.

Best regards.
Rate this:
Please Sign up or sign in to vote.

Solution 1

You are over complicating it does everything itself you don't need co-ordinates at all and you need to use CreateWindowEx because you want WS_EX_TOPMOST, here try this routine. You have already got the manifest pragma you need if using unicode tooltips and to be safe add #define _WIN32_WINNT 0x0600

/*--------------------------------------------------------------------------
  Pass in any window handle and a tooltip string and this function will set
  the create a tooltip to display on the window if you hover over it.
  -------------------------------------------------------------------------*/
HWND AddToolTip (HWND hWnd,	   // Window handle to put tooltip over 							 
		 TCHAR* tooltip) { // Text the tool tip should say
    TOOLINFO ti;
    HWND TTWnd;

    if (tooltip == 0) return (0);		     // Check we have a tooltip
    InitCommonControls(); 	                     // Check common controls are initialized
    TTWnd = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS,
	NULL, WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON,
	CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
	CW_USEDEFAULT, hWnd, 0, 0, 0);	             // Create tooltip window
    memset(&ti, 0, sizeof(TOOLINFO));	             // Clear structure
    ti.cbSize = sizeof(TOOLINFO);		     // Size of structure
    ti.uFlags = TTF_SUBCLASS;			     // Class is subclass
    ti.hwnd = hWnd;				     // Parent window
    ti.hinst = 0;				     // This instance
    ti.uId = 0;					     // No uid
    ti.lpszText = tooltip;			     // Transfer the text pointer
    GetClientRect (hWnd, &ti.rect);		     // Tooltip to cover whole window
    SendMessage(TTWnd, TTM_ADDTOOL, 0, (LPARAM) &ti);// Add tooltip
    return(TTWnd);                                   // Return the tooltip window
};


You can control if the tooltip shows or not using the TTM_ACTIVATE message and it starts active so you probably need to turn it off after you create it because of how you want to use it to only appear on error.
   
v9
Comments
AlwaysLearningNewStuff 28-May-14 9:24am
   
As far as I can tell, this function is from your article about subclassing edit controls. The problem with those tooltips is that they show at the middle of the control. The ones with ES_NUMBER edit control show at the caret position. That is why I thought to try my implementation above.

Still, after experimenting a little, I saw that in the case of the ES_NUMBER edit control, tooltip disappears after some time. This indicates MS did not use tracking tooltip.

Perhaps I could use your suggestion and use GetCaretPos to somehow adjust the rectangle and then show tooltip. Thank you for helping.
leon de boer 28-May-14 11:16am
   
Ok now I get exactly what you are trying to do let me see what I can do with it, not tried this but it shouldn't be hard
Rate this:
Please Sign up or sign in to vote.

Solution 2

Okay it was dead simple to do what you want essentially I just made a static tooltip

So a few changes here, I passed in a TOOLINFO pointer because you will need the data
to turn the tip tracking on and beside that the tooltip form is change to TTF_TRACK.
HWND StaticToolTip(HWND hWnd, // Handle for window to put tooltip over 
     TOOLINFO* ti,                 // Pointer to tooltip data
     TCHAR* tooltip) {		   // Text the tool tip should say
 
HWND TTWnd;

	if (tooltip == 0) return (0);		// Check we have a tooltip
	InitCommonControls(); 	     		// Check common controls are initialized
	TTWnd = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS,
		NULL, WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, hWnd, 0, 0, 0);	// Create tooltip window
	memset(ti, 0, sizeof(TOOLINFO));	// Clear structure
	ti->cbSize = sizeof(TOOLINFO);		// Size of structure
	ti->uFlags = TTF_TRACK;			// Class is subclass
	ti->hwnd = hWnd;			// Parent window
	ti->hinst = 0;				// This instance
	ti->uId = 0;				// No uid
	ti->lpszText = tooltip;			// Transfer the text pointer
	GetClientRect(hWnd, &ti->rect);		// Tooltip to cover whole window
	SendMessage(TTWnd, TTM_ADDTOOL, 0, (LPARAM) ti);// Add tooltip
	return(TTWnd);                          // Return the tooltip window
};


So for my play code I have globals and that data is filled when I create the tooltip
on window handle Wnd
TOOLINFO g_ti;
HWND g_Tw;

g_Tw = StaticToolTip(Wnd, &g_ti, TEXT("I am a static tooltip"));


Okay that done to show the tip you first position the tooltip in absolute screen position
and then turn it on to display using 2 send messages
SendMessage(g_Tw, TTM_TRACKPOSITION, 0, MAKELPARAM(100, 100));  // screen position
SendMessage(g_Tw, TTM_TRACKACTIVATE, TRUE, (LPARAM) &g_ti);     // Turn it on

To turn it off
SendMessage(g_Tw, TTM_TRACKACTIVATE, FALSE, (LPARAM) 0);    // Turn it off


Now my screen position was hard coded at 100,100 but you want it at the caret position so
you need to do a GetCaretPos call which gives the position of caret to the client window
and then a GetWindowRect call which gives the position of the window to the screen.
Add the values together and that is your position to set the tooltip so it looks like this

POINT Cp;
RECT Rc;
GetCaretPos(Wnd, &Cp);   // Get caret position
GetWindowRect(Wnd, &Rc); // Get window position
Rc.left += Cp.x;         // Caret x to window x
Rc.top += Cp.y;          // Caret y to window y
SendMessage(g_Tw, TTM_TRACKPOSITION, 0, MAKELPARAM(Rc.left, Rc.top));
SendMessage(g_Tw, TTM_TRACKACTIVATE, TRUE, (LPARAM) &g_ti);

Job done !!!!!
   

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)




CodeProject, 503-250 Ferrand Drive Toronto Ontario, M3C 3G8 Canada +1 416-849-8900 x 100