【问题标题】:Locale aware edit control subclassing for decimal numbers ( format [sign] [xxx...] [decimal separator] [yy...] )十进制数的区域设置感知编辑控件子类化(格式 [sign] [xxx...] [decimal separator] [yy...])
【发布时间】:2014-03-17 04:42:34
【问题描述】:

简介及相关资料:

我有一个edit control,它应该只接受有符号十进制数——类似于-123.456。此外,应该注意区域设置,因为每个国家/地区的小数分隔符都不相同,在美国使用点,而在欧洲使用逗号等等。

我为解决这个问题所做的努力:

到目前为止,我已经使用subclassing 来实现这一点。这是我实现subclassing的逻辑,通过伪代码表示:

if ( ( character is not a [ digit,separator, or CTRL/Shift... ] OR
     ( char is separator and we already have one ) )
{
    discard the character;
}

首先我做了一个辅助函数来判断char数组是否已经有小数点分隔符,像这样:

bool HasDecimalSeparator( wchar_t *test )
{
    // get the decimal separator
    wchar_t szBuffer[5];

    GetLocaleInfo ( LOCALE_USER_DEFAULT, 
                    LOCALE_SDECIMAL, 
                    szBuffer, 
                    sizeof(szBuffer) / sizeof(szBuffer[0] ) );

    bool p = false; // text already has decimal separator?
    size_t i = 0;   // needed for while loop-iterator

    // go through entire array and calculate the value of the p

    while( !( p = ( test[i] == szBuffer[0] ) ) && ( i++ < wcslen(test) ) );

    return p;
}

这是subclassing 程序-我没有考虑减号

LRESULT CALLBACK Decimalni( HWND hwnd, UINT message, 
    WPARAM wParam, LPARAM lParam, 
    UINT_PTR uIdSubclass, 
    DWORD_PTR dwRefData )
{
    switch (message)
    {
    case WM_CHAR:
        {
            // get decimal separator
            wchar_t szBuffer[5];

            GetLocaleInfo ( LOCALE_USER_DEFAULT, 
                LOCALE_SDECIMAL, 
                szBuffer, 
                sizeof(szBuffer) / sizeof(szBuffer[0] ) );

                wchar_t t[50];  // here we store edit control's current text
                memset( &t, L'\0', sizeof(t) );

                // get edit control's current text
                GetWindowText( hwnd, t, 50 );

                // if ( ( is Not a ( digit,separator, or CTRL/Shift... )
                // || ( char is separator and we already have one ) )
                // discard the character

                if( ( !( isdigit(wParam) || ( wParam == szBuffer[0] ) ) 
                    && ( wParam >= L' ' ) )     // digit/separator/... ?
                    || ( HasDecimalSeparator(t)        // has separator?    
                    && ( wParam == szBuffer[0] ) ) )
                {
                    return 0;
                }
            }
            break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
}

一个重要提示:感谢this question 的回答,我能够在我的应用程序中加载当前的用户区域设置。

问题:

有没有更好的方法来实现一个只接受有符号十进制数字并且支持区域设置的编辑控件?

如果subclassing 是唯一的方法,我的代码可以进一步改进/优化吗?

感谢您的时间和帮助。

最好的问候。

附录:

为了进一步帮助您,这里有一个小型演示应用程序,它创建了一个编辑控件,subclasses 它只接受十进制数字-再说一次,我还没有实现减号部分强>:

#include <windows.h>
#include <commctrl.h>
#include <stdlib.h>
#include <locale.h>

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

const wchar_t g_szClassName[] = L"myWindowClass";

bool HasDecimalSeparator( wchar_t *test )
{
    // get the decimal separator
    wchar_t szBuffer[5];

    GetLocaleInfo ( LOCALE_USER_DEFAULT, 
                    LOCALE_SDECIMAL, 
                    szBuffer, 
                    sizeof(szBuffer) / sizeof(szBuffer[0] ) );

    bool p = false; // text already has decimal separator?
    size_t i = 0;   // needed for while loop-iterator

    // go through entire array and calculate the value of the p

    while( !( p = ( test[i] == szBuffer[0] ) ) && ( i++ < wcslen(test) ) );

    return p;
}

LRESULT CALLBACK Decimalni( HWND hwnd, UINT message, 
    WPARAM wParam, LPARAM lParam, 
    UINT_PTR uIdSubclass, 
    DWORD_PTR dwRefData )
{
    switch (message)
    {
    case WM_CHAR:
        {
            // get decimal separator
            wchar_t szBuffer[5];

            GetLocaleInfo ( LOCALE_USER_DEFAULT, 
                LOCALE_SDECIMAL, 
                szBuffer, 
                sizeof(szBuffer) / sizeof(szBuffer[0] ) );

                wchar_t t[50];  // here we store edit control's current text
                memset( &t, L'\0', sizeof(t) );

                // get edit control's current text
                GetWindowText( hwnd, t, 50 );

                // if ( ( is Not a ( digit,separator, or CTRL/Shift... )
                // || ( char is separator and we already have one ) )
                // discard the character

                if( ( !( isdigit(wParam) || ( wParam == szBuffer[0] ) ) 
                    && ( wParam >= L' ' ) )     // digit/separator/... ?
                    || ( HasDecimalSeparator(t)        // has separator?    
                    && ( wParam == szBuffer[0] ) ) )
                {
                    return 0;
                }
            }
            break;
    }
    return DefSubclassProc( hwnd, message, wParam, lParam);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
    case WM_CREATE:
        {
            /************* load current locale settings *************/

            // max. len: language, country, code page

            wchar_t lpszLocale[64+64+16+3] = L""; 
            wchar_t lpszVal[128];

            LCID nLCID = ::GetUserDefaultLCID(); // current LCID for user
            if ( ::GetLocaleInfo( nLCID, LOCALE_SENGLANGUAGE, lpszVal, 128 ) )
            {
                wcscat_s( lpszLocale, 147, lpszVal ); // language
                if ( ::GetLocaleInfo( nLCID, LOCALE_SENGCOUNTRY, lpszVal, 128 ) )
                {
                    wcscat_s( lpszLocale, 147, L"_" ); // append country/region
                    wcscat_s( lpszLocale, 147, lpszVal );

                    if ( ::GetLocaleInfo( nLCID, 
                        LOCALE_IDEFAULTANSICODEPAGE, lpszVal, 128 ) )
                    { 
                        // missing code page or page number 0 is no error 
                        // (e.g. with Unicode)

                        int nCPNum = _wtoi(lpszVal);
                        if (nCPNum >= 10)
                        {
                            wcscat_s( lpszLocale, 147, L"." ); // append code page
                            wcscat_s( lpszLocale, 147, lpszVal );
                        }
                    }
                }
            }
            // set locale and LCID
            _wsetlocale( LC_ALL, lpszLocale );
            ::SetThreadLocale(nLCID);

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

            HWND hEdit1;

            hEdit1 = CreateWindowEx(0, L"EDIT", L"", 
                WS_BORDER | WS_CHILD | WS_VISIBLE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 
                50, 100, 100, 20, 
                hwnd, (HMENU)8001, GetModuleHandle(NULL), NULL);

            SetWindowSubclass( hEdit1, Decimalni, 0, 0);

        }
        break;

    case WM_SETTINGCHANGE:
        if( !wParam && !wcscmp( (wchar_t*)lParam, L"intl" ) )
        {
            // max. len: language, country, code page
            wchar_t lpszLocale[64+64+16+3] = L""; 
            wchar_t lpszVal[128];

            LCID nLCID = ::GetUserDefaultLCID(); // current LCID for user
            if ( ::GetLocaleInfo( nLCID, LOCALE_SENGLANGUAGE, lpszVal, 128 ) )
            {
                wcscat_s( lpszLocale, 147, lpszVal ); // language
                if ( ::GetLocaleInfo( nLCID, LOCALE_SENGCOUNTRY, lpszVal, 128 ) )
                {
                    wcscat_s( lpszLocale, 147, L"_" ); // append country/region
                    wcscat_s( lpszLocale, 147, lpszVal );
                    if ( ::GetLocaleInfo( nLCID, 
                        LOCALE_IDEFAULTANSICODEPAGE, lpszVal, 128 ) )
                    { 
                        // missing code page or page number 0 is no error
                        // (e.g. with Unicode)
                        int nCPNum = _wtoi(lpszVal);
                        if (nCPNum >= 10)
                        {
                             wcscat_s( lpszLocale, 147, L"." ); // append code page
                             wcscat_s( lpszLocale, 147, lpszVal );
                        }
                    }
                 }
             }
             // set locale and LCID
             _wsetlocale( LC_ALL, lpszLocale );
             ::SetThreadLocale(nLCID);

             return 0L;
         }
         else
             break;

    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, L"Window Registration Failed!", L"Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    hwnd = CreateWindowEx(
        0,
        g_szClassName,
        L"theForger's Tutorial Application",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 480, 320,
        NULL, NULL, hInstance, NULL);

    if(hwnd == NULL)
    {
        MessageBox(NULL, L"Window Creation Failed!", L"Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

【问题讨论】:

    标签: c++ winapi locale subclassing editcontrol


    【解决方案1】:

    考虑特定区域设置

    您当然可以自己做所有事情,但是您可以选择使用 VarI4FromStr 或类似的 API 来为您处理脏东西。输入字符串,输出LONG。区域感知。

    “应该只接受”

    您没有指定控件应如何准确执行此操作。如果输入字符串无效怎么办?控制应该仍然接受它,因为例如,字符串还不是有效的并且用户仍在输入。如果您在外部处理程序中验证输入,例如按下 OK 按钮时,那么您甚至不需要子类化。如果您想在每次更改时检查输入,您也不需要子类,因为您在父级上有EN_CHANGE 通知。不过,您可能出于其他原因想要子类化。

    接受任何输入然后在文本更改或输入验证时以某种方式指示有效性(例如,如果无效,则用红色下划线)是用户友好的。

    【讨论】:

    • 关于 "Should accept only" You don't specify how the control should enforce this exactly. 部分:字符串将始终有效,因为编辑控件将接受位/减号/decimal 分隔符,并由subclassing 保护。在 Decimalni subclassing 过程中,我过滤掉了无效字符 - 请参阅我的 WM_CHAR 处理程序(请注意,尽管我没有在那里处理减号)。至于EN_CHANGE,我不会用它来过滤无效字符,缺点可能是控件闪烁。你能告诉我如何用一些伪代码来使用它吗?谢谢。
    • 字符串呢,它只包含减号?你会接受吗?或者,如果不是,用户如何输入负值,他应该先输入数字,然后在后面加上减号前缀?如果我想从剪贴板“-1.2345 foo”粘贴,然后删除使字符串无效的所有内容怎么办?问题是限制用户活动可能非常烦人。
    • 子类化当然是一个强大的选项。如果我上面的观点没有说服你,你当然想限制输入的字符。我想如果你想要它干净且不闪烁,你将不得不子类化,但你还必须涵盖更多场景:区域设置更改为控件中的非空字符串、2+ 小数分隔符、除第一个字符以外的减号,怎么样每个区域设置的千位分隔符?
    • 经过深入研究和思考,我改变了主意,决定接受您的建议。事实上,最好的办法是在回复EN_CHANGE 时指出错误,因为它是用户友好的。我相信弹出tooltip可以为我提供的不仅仅是改变画笔颜色。将控件子类化以接受WM_CHAR 上的有效字符也是很好的保护。谢谢您的帮助。 +1。我很抱歉花了这么长时间来识别最佳解决方案。最好的问候。
    【解决方案2】:

    在考虑了Advice required to insert one string into another once obtaining text from clipboard 的代码之后,我能够提出一个满足要求的子类化过程。

    我的解决方案的重点是模拟该帖子中提到的编辑控件的行为,然后验证生成的文本。

    在处理VK_DELETE 时,删除选定的文本,然后解析结果以检查是否留下有效的十进制格式。如果一切正常,则将消息传递给默认过程,否则将被丢弃。在WM_CHAR 处理程序中对WM_CUTWM_CLEARbackspace 执行相同的方法(这里我们必须通过访问带有序数-1 的字符串元素来保护自己免于崩溃应用程序,即这就是我添加if ( start &gt; 0 ) 行的原因。

    当处理WM_PASTE 我们merge the edit control's text with the clipboard text 然后我们解析结果字符串以检查其有效性。同样,如果一切正常,我们传递消息,否则我们丢弃它。

    同样的事情也适用于WM_CHAR,只是我们在这里在编辑控件文本的选定部分插入字符,然后执行有效性检查。

    由于这样输入的文本总是正确的,我们不必处理WM_UNDO

    最后,代码如下:

    LRESULT CALLBACK Decimalni( HWND hwnd, UINT message, 
        WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
    {
    
        switch (message)
        {
        case WM_KEYDOWN:
            {
                if( wParam == VK_DELETE )
                {
                    DWORD start, end;
    
                    int len = GetWindowTextLength(hwnd);
    
                    std::wstring buffer( len, 0 );
    
                    // get current window text
    
                    if( len > 0 )
                       GetWindowText( hwnd, &buffer[0], len + 1 );
    
                    // get current selection
                    SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
    
                    if( end > start )
                        buffer.erase( start, end - start );
                    else
                        buffer.erase( start, 1 );
    
                    if( buffer.empty() )
                        return ::DefSubclassProc( hwnd, message, wParam, lParam);
    
                    bool IsTextValid = true; // indicates validity of inputed text
    
                    // TODO: parse buffer
    
                    if( IsTextValid )
                         return ::DefSubclassProc( hwnd, message, wParam, lParam);
                    else
                    {
                         // TODO: indicate error
                         return FALSE;
                    }
                }
            }
            return ::DefSubclassProc( hwnd, message, wParam, lParam);;
            break;
        case WM_CLEAR:
        case WM_CUT:
            {
                DWORD start, end;
    
                int len = GetWindowTextLength(hwnd);
    
                std::wstring buffer( len, 0 );
    
                // get current window text
    
               if( len > 0 )
                   GetWindowText( hwnd, &buffer[0], len + 1 );
    
                // get current selection
                SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
    
                if( end > start )
                    buffer.erase( start, end - start );
    
                if( buffer.empty() )
                    return ::DefSubclassProc( hwnd, message, wParam, lParam);
    
                // TODO: parse buffer 
                bool IsTextValid = true;
    
                if( IsTextValid )
                    return ::DefSubclassProc( hwnd, message, wParam, lParam);
                else
                {
                    // TODO: Indicate error
                    return FALSE;
                }
            }
            break;
        case WM_PASTE:
            {
                int len = GetWindowTextLength(hwnd);
    
                std::wstring clipboard, wndtxt( len, 0 );
    
                if( len > 0 )
                    GetWindowText( hwnd, &wndtxt[0], len + 1 );
    
                if( !OpenClipboard(hwnd) )
                    return FALSE;
    
                HANDLE hClipboardData;
    
                if( hClipboardData = GetClipboardData(CF_UNICODETEXT) )
                {
                     clipboard = (wchar_t*)GlobalLock(hClipboardData);
                     GlobalUnlock(hClipboardData);  
    
                }
    
                CloseClipboard();
    
                if( clipboard.empty() )
                    return FALSE;
    
                DWORD start, end;
                SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
    
                // merge strings into one
                if( end > start )
                   wndtxt.replace( start, end - start, clipboard );
                else
                    wndtxt.insert( start, clipboard );
    
                // TODO: parse the text
                bool ITextValid = true;
    
                // process the result
                if( IsTextValid )
                    return ::DefSubclassProc( hwnd, message, wParam, lParam);
                else
                {
                    // TODO: indicate error
                    return FALSE;
                }
    
            }
            break;
        case WM_CHAR:
            {
                DWORD start, end;
    
                int len = GetWindowTextLength(hwnd);
    
                std::wstring buffer( len, 0 );
    
                // get current window text
    
                if( len > 0 )
                    GetWindowText( hwnd, &buffer[0], len + 1 );
    
                // get current selection
                SendMessage( hwnd, EM_GETSEL, (WPARAM)&start, (LPARAM)&end );
    
                // allow copy/paste but leave backspace for special handler
                if( ( wParam < 0x020 ) && ( wParam != 0x08 ) )
                    return ::DefSubclassProc( hwnd, message, wParam, lParam);}
    
                // process backspace
                if( wParam == 0x08 ) 
                {
                    if( end > start )
                        buffer.erase( start, end - start );
                    else
                        if( start > 0 )    // it is safe to move back one place
                            buffer.erase( start - 1, 1 );
                        else  // start-1 < 0 , can't access buffer[-1] !!
                            return FALSE;
    
                    if( buffer.empty() )
                        return ::DefSubclassProc( hwnd, message, wParam, lParam);
    
                    // TODO: parse buffer
    
                    // process the result
                    if( IsTextValid )
                         return ::DefSubclassProc( hwnd, message, wParam, lParam);
                    else
                    {
                         //TODO: indicate error
                         return FALSE;
                    }
                }
    
                // insert character and parse text
    
                if( end > start )
                    buffer.replace( start, end - start, 1, (wchar_t)wParam );
                else
                    buffer.insert( start, 1, (wchar_t)wParam );
    
                // TODO: parse text
    
                // process the result
                if( IsTextValid )
                    return ::DefSubclassProc( hwnd, message, wParam, lParam);
                else
                {
                    //TODO: indicate error
                    return FALSE;
                }
            }
            break;
        case WM_NCDESTROY:
            ::RemoveWindowSubclass( hwnd, Decimalni, 0 );
            break;
        }
        return ::DefSubclassProc( hwnd, message, wParam, lParam);
    }
    

    【讨论】:

      猜你喜欢
      • 2010-10-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-13
      • 1970-01-01
      • 1970-01-01
      • 2013-08-22
      • 1970-01-01
      相关资源
      最近更新 更多