在编辑控件中显示无效输入的工具提示

时间:2014-05-27 14:52:12

标签: c++ winapi tooltip editcontrol

我已将子编辑控件子类化为仅接受浮动数字。我想在用户输入无效时弹出工具提示。我定位的行为就像ES_NUMBER的一个编辑控件具有:

enter image description here

到目前为止,我能够实现跟踪工具提示并在用户进行无效输入时显示它。

但是,工具提示是错误的。我尝试使用ScreenToClientClientToScreen来修复此问题,但失败了。

以下是创建 SCCE 的说明:

1)在Visual Studio中创建默认的Win32项目。

2)在stdafx.h下方#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)添加这些全局变量:

HWND g_hwndTT;
TOOLINFO g_ti;

4)这是编辑控件的简单子类过程(仅用于测试目的):

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)添加以下WM_CREATE处理程序:

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)在MyRegisterClassreturn语句之前)初始化常用控件:

// 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 

SSCCE

我的问题如下:

  1. 如何在主窗口中正确放置工具提示?我应该如何使用插入符坐标进行操作?

  2. 工具提示句柄和工具信息结构是否有办法不是全局的?

  3. 感谢您的时间。

    祝你好运。

    编辑#1:

    通过删除子类过程中的ScreenToClient调用,我设法取得了相当大的进步。 x坐标很好,y坐标可能略低。我仍然想以某种方式删除全局变量......

    编辑#2:

    我可以使用EM_GETRECT消息调整y坐标,并将y坐标设置为格式化矩形的底部:

    RECT rcClientRect;
    Edit_GetRect( hwnd, &rcClientRect );
    pt.y = rcClient.bottom;
    

    现在最终结果要好得多。剩下的就是删除全局变量......

    编辑#3:

    好像我已经破解了!该解决方案位于EM_SHOWBALLOONTIPEM_HIDEBALLOONTIP条消息中!工具提示位于插入符号位置,气球形状与图片上的气球形状相同,并且它会自动自动解散。最好的是我不需要全局变量!

    这是我的子类程序片段:

    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;
    

3 个答案:

答案 0 :(得分:4)

我将评论作为答案(我之前应该这样做),以便明确问题得到解答:

MSDN Docs for TTM_TRACKPOSITION表示x / y值是“屏幕坐标”。

我不完全确定,但y坐标可能对应于插入符号的顶部,如果要将工具提示放在编辑框的中间,可以添加编辑框高度的一半。

修改 re全局变量,您可以将所有全局变量捆绑到一个结构中,为结构分配内存,并使用GWLP_USERDATA编辑窗口的SetWindowLongPtr API调用传递结构的指针,窗口{{1}}然后,proc可以使用GetWindowLongPtr ...

检索值

答案 1 :(得分:4)

经过进一步测试后,我决定将此作为答案,以便其他人可以清楚地发现它。

解决方案是使用EM_SHOWBALLOONTIPEM_HIDEBALLOONTIP消息。 您不需要创建工具提示并将其与编辑控件相关联!因此,我现在需要做的只是子类编辑控件,一切正常:

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);
} 

那就是它!

希望这个答案也能帮助别人!

答案 2 :(得分:2)

作为关于使用SetProp函数的注释的后续内容,我不需要保留一对全局变量用于工具提示数据,我提出了以下解决方案。

注意:通过对GetProp的调用进行错误检查,我为子类编辑控件设计了一个WndProc,无论是否需要使用工具提示,它都可以运行。如果找不到该属性,我只省略任何工具提示处理代码。

注2:使工具提示信息非全局化的所有可用方法的一个缺点是它引入了子类WndProc和父窗口的wndProc之间的耦合。

  • 使用dwRefData,必须检查它是否包含非NULL 指针。
  • 使用SetWindowLongPtr,必须记住索引 用户数据。
  • 使用SetProp,必须记住文本属性名称。我发现 这更容易。

删除对SetProp的调用会删除工具提示功能。即,您是否可以使用相同的子类wndProc进行编辑控制,无论它们是否利用了工具提示。

Anyhoo,使用(Code :: Blocks)代码。

#define _WIN32_IE 0x0500
#define _WIN32_WINNT 0x0501

#if defined(UNICODE) && !defined(_UNICODE)
    #define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
    #define UNICODE
#endif

#include <tchar.h>
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <ctype.h>
#include <cstdio>

/*  Declare Windows procedure  */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);

/*  Make the class name into a global variable  */
TCHAR szClassName[ ] = _T("CodeBlocksWindowsApp");



HWND g_hwndTT;
TOOLINFO g_ti;
typedef struct mToolTipInfo
{
    HWND hwnd;
    TOOLINFO tInfo;
} * p_mToolTipInfo;


LRESULT CALLBACK EditSubProc ( HWND hwnd, UINT message,
    WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    p_mToolTipInfo tmp = (p_mToolTipInfo)GetProp(hwnd, _T("tipData"));

    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 );

                    RECT lastCharRect;
                    lastCharRect.left = lastCharRect.top = 0;
                    lastCharRect.right = lastCharRect.bottom = 32;

                    HDC editHdc;
                    char lastChar;
                    int charHeight, charWidth;

                    lastChar = (char)wParam;
                    editHdc = GetDC(hwnd);
                    charHeight = DrawText(editHdc, &lastChar, 1, &lastCharRect, DT_CALCRECT);
                    charWidth = lastCharRect.right;
                    ReleaseDC(hwnd, editHdc);

                    //pt.x += xOfs + charWidth; // invalid char isn't drawn, so no need to advance xPos to reflect width of last char
                    pt.y += charHeight;

                    if (tmp)
                    {
                        SendMessage(tmp->hwnd, TTM_TRACKACTIVATE, TRUE, (LPARAM)&tmp->tInfo);
                        SendMessage(tmp->hwnd, TTM_TRACKPOSITION, 0, MAKELPARAM(pt.x, pt.y));
                    }
                }
                return FALSE;
            }
            else
            {
                if (tmp)
                    SendMessage(tmp->hwnd, TTM_TRACKACTIVATE,
                    FALSE, (LPARAM)&tmp->tInfo  );
                return ::DefSubclassProc( hwnd, message, wParam, lParam );
            }
        }
        break;

    case WM_DESTROY:
        {
            p_mToolTipInfo tmp = (p_mToolTipInfo)GetProp(hwnd, _T("tipData"));
            if (tmp)
            {
                delete(tmp);
                RemoveProp(hwnd, _T("tipData"));
            }
        }
        return 0;

    case WM_NCDESTROY:
        ::RemoveWindowSubclass( hwnd, EditSubProc, 0 );
        return DefSubclassProc( hwnd, message, wParam, lParam);
        break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
}






HINSTANCE hInst;

int WINAPI WinMain (HINSTANCE hThisInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR lpszArgument,
                     int nCmdShow)
{
    HWND hwnd;               /* This is the handle for our window */
    MSG messages;            /* Here messages to the application are saved */
    WNDCLASSEX wincl;        /* Data structure for the windowclass */

    /* The Window structure */
    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;      /* This function is called by windows */
    wincl.style = CS_DBLCLKS;                 /* Catch double-clicks */
    wincl.cbSize = sizeof (WNDCLASSEX);

    /* Use default icon and mouse-pointer */
    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;                 /* No menu */
    wincl.cbClsExtra = 0;                      /* No extra bytes after the window class */
    wincl.cbWndExtra = 0;                      /* structure or the window instance */
    /* Use Windows's default colour as the background of the window */
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

    /* Register the window class, and if it fails quit the program */
    if (!RegisterClassEx (&wincl))
        return 0;

    /* The class is registered, let's create the program*/
    hwnd = CreateWindowEx (
           0,                   /* Extended possibilites for variation */
           szClassName,         /* Classname */
           _T("Code::Blocks Template Windows App"),       /* Title Text */
           WS_OVERLAPPEDWINDOW, /* default window */
           CW_USEDEFAULT,       /* Windows decides the position */
           CW_USEDEFAULT,       /* where the window ends up on the screen */
           544,                 /* The programs width */
           375,                 /* and height in pixels */
           HWND_DESKTOP,        /* The window is a child-window to desktop */
           NULL,                /* No menu */
           hThisInstance,       /* Program Instance handler */
           NULL                 /* No Window Creation data */
           );

    /* Make the window visible on the screen */
    ShowWindow (hwnd, nCmdShow);

    /* Run the message loop. It will run until GetMessage() returns 0 */
    while (GetMessage (&messages, NULL, 0, 0))
    {
        /* Translate virtual-key messages into character messages */
        TranslateMessage(&messages);
        /* Send message to WindowProcedure */
        DispatchMessage(&messages);
    }

    /* The program return-value is 0 - The value that PostQuitMessage() gave */
    return messages.wParam;
}


/*  This function is called by the Windows function DispatchMessage()  */
LRESULT CALLBACK WindowProcedure (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)                  /* handle the messages */
    {
        case WM_CREATE:
        {
            HWND hEdit = CreateWindowEx( 0, _T("EDIT"), _T("edit"), WS_CHILD | WS_VISIBLE |
                WS_BORDER | ES_CENTER, 150, 150, 100, 30, hWnd, (HMENU)1000, hInst, 0 );

            p_mToolTipInfo tmp = new mToolTipInfo;
            SetProp(hEdit, _T("tipData"), tmp);

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

            //if( !g_hwndTT )
            if( !tmp->hwnd )
                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 = _T("Hi there");
            tmp->tInfo.cbSize = sizeof(TOOLINFO);
            tmp->tInfo.uFlags = TTF_TRACK | TTF_ABSOLUTE;
            tmp->tInfo.hwnd = hWnd;
            tmp->tInfo.hinst = hInst;
            tmp->tInfo.lpszText = _T("Hi there");

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

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

        case WM_DESTROY:
            PostQuitMessage (0);       /* send a WM_QUIT to the message queue */
            break;
        default:                      /* for messages that we don't deal with */
            return DefWindowProc (hWnd, message, wParam, lParam);
    }

    return 0;
}