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 :
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 :
How can I properly position tooltip in my main window? How should I manipulate with caret coordinates?
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;
ScreenToClient
I have placed it properly. I wish y-coordinate to be a little lower, so maybe I should just addpt.y += 10
before passing it toClientToScreen
... Still, I do not know how to get rid of global variables... Thank you for pointing out what I have missed. Best regards. – SideroDrawText
with theDT_CALCRECT
flag, you can get the height of the last entered char (it's the same value for both.
andI
). If you add this to the yPos, you get the same yPos as the image you've shown. I agree with @EdwardClements, that a good place to stuff the info would be into the USERDATA field of the editWnd. However, I personally would use theSetProp
function for this task, it's much cleaner and clearer imho. (Dont forget to callRemoveProp
before destroying the window) – PantaletsdwRefData
in my call toSetWindowSubclas
API. I am interested to apply Edward's solution but I will need help, since this will be my first time to do this. If you are interested to help I would appreciate it. Thank you for your time :) – SiderodwRefData
variable requires that you keep a reference to the data in the parent window's WndProc. Anyway, there's all kinds of alternatives. I'll post my complete (C::B) code as an 'answer' to ensure completeness. – Pantalets