Locale aware edit control subclassing for decimal numbers ( format [sign] [xxx...] [decimal separator] [yy...] )
Asked Answered
W

2

5

INTRODUCTION AND RELEVANT INFORMATION:

I have an edit control that should accept only signed decimal numbers-something like -123.456. Also, it should be locale aware, since decimal separator is not the same for every country-in US dot is used, while in Europe it is comma and so on.

MY EFFORTS TO SOLVE THIS:

So far I have used subclassing to implement this. Here is my logic for implementing the subclassing, expressed through pseudo code:

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

First I have made a helper function that determines if the char array already has a decimal separator, like this:

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

And here is the subclassing procedure-I haven't taken minus sign into account:

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

One important note: I am able to load current user locale settings in my application, thanks to the answers to this question.

QUESTION:

Is there a better way to implement an edit control that accepts signed decimal numbers only, and is locale aware?

If subclassing is the only way, can my code be further improved/optimized ?

Thank you for your time and help.

Best regards.

APPENDIX:

To help you even further, here is a small demo application that creates an edit control and subclasses it to accept only decimal numbers-again, I haven't implemented the part for the minus sign:

#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;
}
Whitewing answered 16/2, 2014 at 13:4 Comment(0)
D
5

Taking into consideration locale-specific settings

You certainly can do everything yourself, however you have an option to use VarI4FromStr or similar API which does dirty stuff for you. You put string in, you get LONG out. Locale aware.

"Should accept only"

You don't specify how the control should enforce this exactly. What if the input string is not valid? Control should be still accepting it because, for example, string is just not yet valid and user is still typing. If you are validating the input in external handler, such as when OK button is pressed, then you don't even need to subclass. If you want to check input every time it changes, you don't need to subclass either since you have EN_CHANGE notifications on parent. You might want to subclass for other reasons though.

It is user-friendly to accept any input and then indicate validity somehow (such as underlining with red if invalid) either on text change or on input validation.

Duwe answered 16/2, 2014 at 15:14 Comment(4)
About the "Should accept only" You don't specify how the control should enforce this exactly. part: String will always be valid because edit control will accept only digits/minus/decimal separator and that is secured with subclassing. In the Decimalni subclassing procedure I filter out invalid characters-see my WM_CHAR handler ( note though that I did not handle minus sign there ). As for EN_CHANGE, I do not how to use it to filter out invalid characters, and the downside might be flickering of the control.Can you show me how to use it with some pseudocode? Thank you.Whitewing
What about string, which consists of minus only? Are you going to accept that? Or, if no, how would user enter a negative value, should he enter digits first, and then prefix the thing with minus sign later? What if I want to paste from clipboard " -1.2345 foo " and then delete everything that makes the string invalid? The thing is that limiting user activity might be extremely annoying.Duwe
Subclassing is of course a powerful option that you have. And you certainly want to limit the entered characters if my point above did not persuade you. I suppose you will have to subclass if you want it clean and flickerless, but you will also have to cover more scenarios: locale changed with non-empty string in the control, 2+ decimal separators, minus sign otherwise than first character, what about thousand separator per locale settings?Duwe
I have changed my mind after thorough research and thinking and have decided to accept your suggestion. Indeed the best thing is to indicate error in response to EN_CHANGE since it is user friendly. I believe that poping a tooltipcan offer me more than simply changing the brush color. Subclassing the control to accept valid characters on WM_CHAR is also good protection. Thank you for your help. +1. I apologize for taking this long to recognize the best solution. Best regards.Whitewing
H
3

After taking into account code from Advice required to insert one string into another once obtaining text from clipboard, I was able to put up a subclassing procedure that meets the requirement.

The point of my solution is to simulate edit control's behavior as mentioned in that post and then validate the resulting text.

When handling VK_DELETE, selected text is deleted and the result is then parsed to check if valid decimal format is left. If everything is OK message is passed to the default procedure else is discarded. The same method is performed for WM_CUT, WM_CLEAR, and for backspace in the WM_CHAR handler ( here we must protect ourself from crashing the app by accessing the element of the string with the ordinal -1, that is the reason why I have added the line if ( start > 0 ) ).

When handling WM_PASTE we merge the edit control's text with the clipboard text and then we parse the resulting string to check its validity. Again, if all is OK we pass the message else we discard it.

The same thing applies for WM_CHAR except we here insert character in the selected part of the edit control's text and then we perform the validity check.

Since the inputted text will be always correct this way, we do not have to handle WM_UNDO.

Finaly, here is the code:

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);
}
Hilaryhilbert answered 16/3, 2014 at 19:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.