c++winapitooltipeditcontrol

Show tooltip on invalid input in edit control


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 :

enter image description here

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;
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 #1 ****************************/
                    /******* If I delete this line x-coordinate is OK *************/
                    /*** y-coordinate should be little lower, but it is still OK **/
                    /**************************************************************/

                    ScreenToClient( GetParent(hwnd), &pt );

                    /************************* Edit #2 ****************************/

                    // this adjusts the y-coordinate, see the second edit
                    RECT rcClientRect;
                    Edit_GetRect( hwnd, &rcClientRect );
                    pt.y = rcClientRect.bottom;

                    /**************************************************************/

                    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;

Solution

  • I'm giving the comment as an answer (I should have done that earlier) so that it's clear that the question has been answered:

    MSDN Docs for TTM_TRACKPOSITION says that the x/y values are "in screen coordinates".

    I'm not totally sure, but the y-coordinate probably corresponds to the top of the caret, you could add half of the edit box height if you want to position your tooltip in the middle of the edit box.

    EDIT re Global variables, you could bundle all your global variables into a structure, allocate memory for the structure and pass the pointer of the structure using the SetWindowLongPtr API call for the edit window using the GWLP_USERDATA, the window proc can then retrieve the values using GetWindowLongPtr...