【问题标题】:Show tooltip on invalid input in edit control在编辑控件中显示无效输入的工具提示
【发布时间】:2014-07-16 13:17:17
【问题描述】:

我将编辑控件子类化为只接受浮点数。当用户输入无效时,我想弹出一个工具提示。我的目标行为就像一个带有ES_NUMBER 的编辑控件有:

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

但是,工具提示放错了位置。我曾尝试使用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) 初始化MyRegisterClass中的常用控件(return语句之前):

// 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. 有没有办法让工具提示句柄和工具信息结构不是全局的?

感谢您的宝贵时间。

最好的问候。

编辑#1:

通过删除子类过程中的ScreenToClient 调用,我已经设法实现了相当大的改进。 x坐标很好,y坐标可以稍微低一些。我仍然想以某种方式删除全局变量...

编辑#2:

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

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

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

编辑#3:

看来我已经破解了!解决方案在EM_SHOWBALLOONTIPEM_HIDEBALLOONTIP 消息中!工具提示放置在插入符号位置,气球形状与图片上的相同,并且它会正确自动关闭。最棒的是我不需要全局变量!

这是我的子类过程 sn-p:

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;

【问题讨论】:

  • MSDN Docs for TTM_TRACKPOSITION 表示 x/y 值是“在屏幕坐标中”
  • @EdwardClements:确实,在删除ScreenToClient 之后,我已经正确放置了它。我希望y坐标低一点,所以也许我应该在传递给ClientToScreen之前添加pt.y += 10...不过,我不知道如何摆脱全局变量...谢谢您的指点我错过了什么。最好的问候。
  • 如果您使用带有DT_CALCRECT 标志的DrawText,您可以获得最后输入的字符的高度(.I 的值相同)。如果将此添加到 yPos,您将获得与您显示的图像相同的 yPos。我同意@EdwardClements,将信息填充到editWnd 的USERDATA 字段的好地方。 然而,我个人会使用SetProp 函数来完成这项任务,它更加更干净、更清晰恕我直言。 (在销毁窗口之前不要忘记致电RemoveProp
  • @enhzflep:我已经设法获得了第二次编辑中描述的正确坐标。过去,您曾建议我在调用SetWindowSubclas API 时将结构作为dwRefData 发送。我有兴趣应用 Edward 的解决方案,但我需要帮助,因为这是我第一次这样做。如果您有兴趣提供帮助,我将不胜感激。谢谢你的时间:)
  • 我确实注意到了,只是想我会添加我认为可以重现图像的代码。使用 SetProp 的另一个优点是控件可以删除信息本身(而不是父 wnd 必须这样做)通过dwRefData 变量传递它要求您保留对父窗口 WndProc 中数据的引用。无论如何,有各种各样的选择。我会将完整的 (C::B) 代码发布为“答案”以确保完整性。

标签: c++ winapi tooltip editcontrol


【解决方案1】:

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

解决方案是使用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】:

    作为 cmets 关于使用 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;
    }
    

    【讨论】:

    • 感谢您的帮助。赞成。我更有兴趣根据您对this question 的回答来调整您的建议。根据这些答案,我决定验证EN_UPDATE 中的输入并将控件子类化以丢弃无效字符。在EN_UPDATE 处理程序中,我还更改了编辑控件的颜色以指示有效性。我只是想弹出一个工具提示,在无效字符上列出 locale 有效字符。合并此解决方案将有点困难...
    • 不客气。谢谢。嗯,当我启动一台“真正的”电脑(刚才使用 rasPi)时,我肯定会看一下。我想实现一个附加了特定于语言环境的字符的标准消息应该不会太难。类似:string msg = "only valid (0-9) and "; string += localeSpecificCharsString;。可能只是一个或三个额外的字段来添加到特定于实例的数据中。我会看看和思考。当我写这篇文章时甚至没有考虑过这个问题 - 哎呀!
    • ES_NUMBER编辑控件后我看到它有不同形状的气球提示和气球提示一段时间后消失。这意味着MS没有使用跟踪工具提示来实现它,但可能将普通工具提示绑定到一个矩形。除此之外,我运行了您的示例,它的性能与我的相同。这让我想知道我的第二次编辑方法是否比通过“获取 HDC、DrawText 等”更好,因为我得到 格式化矩形 - 请参阅我链接到的文档......我会尝试找到一种在不跟踪工具提示的情况下实现该行为的方法。再次感谢!
    • 太棒了!布拉沃和祝贺。已添加书签以供将来参考。
    • 感谢您的所有帮助和努力。 '直到下一次...... :)
    【解决方案3】:

    我将评论作为答案(我应该早点这样做),以便清楚地回答问题:

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

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

    编辑 重新全局变量,您可以将所有全局变量捆绑到一个结构中,为该结构分配内存并使用 SetWindowLongPtr API 调用使用 GWLP_USERDATA 为编辑窗口传递结构的指针,然后窗口 proc 可以检索使用GetWindowLongPtr的值...

    【讨论】:

    • 我刚刚用新发现更新了帖子。关于工具提示坐标,我几乎“在那里”。我仍然无法弄清楚如何摆脱全局变量......现在+1。
    • 当我使用SetWindowSubclass 时,我可以传递结构,因为通常使用第 4 个参数......我只是有点困惑:因为我将使用这个过程来继承多个编辑控件我担心工具提示会有一些副作用......在我测试你的方法和我的方法之前,我需要休息一下。谢谢你的帮助。最好的问候。
    • 到目前为止,我从未使用过成员 enhzflep 提出的方式,也没有使用过你的方式,所以如果你能帮助我提供一些伪代码,我将不胜感激。
    • 我会尝试自己解决问题。你的回答涵盖了我所需要的。不过,如果您能够帮助我正确使用 SetPropSetWindowPtr,我将不胜感激。
    • 希望没事。刚刚意识到我应该将if (tmp) 放在WM_CHAR 案例中,这样它不仅可以控制消息的发送,还可以控制坐标计算代码的发送。
    猜你喜欢
    • 1970-01-01
    • 2018-10-26
    • 2010-09-07
    • 2014-07-31
    • 2014-02-07
    • 2014-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多