3

On my current project I have been struggling the last couple of days with subclassing edit boxes. By now I successfully subclassed my edit box and validated the input such that it only accepts digits, commas, a minus sign and keyboard commands.

But for quite a while I'm now stuck with the refinement of the input validation. I want my edit box to behave as the following:

  • accept minus sign only at first position
  • accept only one leading zero
  • accept only one comma
  • force comma after leading zero
  • manage those cases when deleting single characters or selections of the text via 'back', 'delete', select-all and then pasting something over it

My code in its current form looks like this and provides almost none of the advanced validation requirements I specified above:

inline LRESULT CALLBACK decEditBoxProc(HWND hWnd,
                                       UINT msg, 
                                       WPARAM wParam, 
                                       LPARAM lParam,
                                       UINT_PTR uIdSubclass,
                                       DWORD_PTR dwRefData)
{
    if(msg == WM_CHAR)
    {
        decEditBoxData* data = reinterpret_cast<decEditBoxData*>(ULongToPtr(dwRefData));

        bool isDigit          = (wParam >= '0' && wParam <= '9');
        bool isZero           = ((wParam == '0') && !data->blockZero);
        bool isSign           = (wParam == '-');
        bool isComma          = ((wParam == '.' || wParam == ',') && !data->blockComma);
        bool isValidCommand   = (wParam == VK_RETURN  
                                || wParam == VK_DELETE 
                                || wParam == VK_BACK);


        // Restrict comma to one.
        if(isComma && data->nCommas > 0)
            return FALSE;
        else if(isComma && data->nCommas == 0)
            data->nCommas++;

        // Restrict trailing zeroes to one.
        if(isZero && data->nTrailingZeroes > 0)
            return FALSE;
        else if(isZero && data->nTrailingZeroes == 0)
            data->nTrailingZeroes++;

        // Filter everything but digits, commas and valid commands.
        if(!isDigit && !isValidCommand && !isComma)
            return FALSE;
    }
    return DefSubclassProc(hWnd, msg, wParam, lParam);
}

Any idea on how to algorithmically solve this problem is very appreciated.

UPDATE

Thanks to the suggestions of David Heffernan and IInspectable I was able to (almost) solve my problem without subclassing the edit controls.

In the dialog procedure (thats contains the edit controls):

switch(msg)
{
case WM_COMMAND:
   switch(LOWORD(wParam))
   {
      case IDC_IN_REAL:
         if(HIWORD(wParam)==EN_CHANGE) onEditChange(hDlg, IDC_IN_REAL);
         break;

      case IDC_IN_IMAG:
         if(HIWORD(wParam)==EN_CHANGE) onEditChange(hDlg, IDC_IN_IMAG);
         break;
    }
    break;
}

With onEditChange:

void onEditChange(HWND hDlg, int ctrlID)
{
    HWND hEdit    = GetDlgItem(hDlg, ctrlID);
    size_t len    = GetWindowTextLength(hEdit)+1;
    wchar_t* cstr = new wchar_t[len];
    GetWindowText(hEdit, cstr, len);

    std::wstring wstr(cstr);

    if(!(tools::isFloat(wstr)))
    {
        EDITBALLOONTIP bln;
        bln.cbStruct = sizeof(EDITBALLOONTIP);
        bln.pszTitle = L"Error";
        bln.pszText  = L"Not a valid floating point character.\nUse '.' instead of ','";
        bln.ttiIcon  = TTI_ERROR;
        Edit_ShowBalloonTip(hEdit, &bln);
    }
    delete [] cstr;
}

and isFloat():

bool tools::isFloat(std::wstring str)
{
    std::wistringstream iss(str);
    float f;
    wchar_t wc;
    if(!(iss >> f) || iss.get(wc))
        return false;
    return true;
}

I will probably add some more visual feedback for the user, but that's not important right now.

The question however, is not answered yet. My intention was to allow the "," as a possible decimal point.

Ale
  • 1,727
  • 14
  • 26
0x492559F64E
  • 124
  • 1
  • 13
  • 5
    This is a really bad idea. Don't block input. There are many many different ways for the user to do input data, it's really hard for you to block them all. You are currently nowhere near doing so. It is also terribly frustrating to have controls that won't accept input without any feedback as to why not. Let the user enter whatever they like, and validate later, when you need to use the value. – David Heffernan Jun 20 '14 at 10:41
  • 2
    Steps to solve your problem: 1.) Handle the `EN_CHANGE` notification. It is sent whenever the user has taken an action that may have altered the text. 2.) Validate input. This is easy: `bool bValid = (swscanf(buf,L"%f",&f)==wcslen(buf));`. 3.) Provide non-intrusive feedback, e.g. changing the control's background color, and disable the dialog's OK button. – IInspectable Jun 20 '14 at 12:33
  • Thank you for your suggestions so far. I was planning on implementing some form of notification later on but for now I need to solve the inupt validation first. – 0x492559F64E Jun 20 '14 at 14:02
  • @IInspectable thank you for your hint on swscanf and EN_CHANGE – 0x492559F64E Jun 20 '14 at 14:07
  • @IInspectable Hm ... I didn't have much time to try it out thoroughly yet but it seems `EN_CHANGE` doesn't get called. I checked `msg==WM_COMMAND` first and `HIWORD(wParam)==EN_CHANGE` right after. But my test output inside doesn't get called when using the edit control. Any idea what's wrong? I have to check, but I don't think that I set my edit controls to `ES_MULTILINE` – 0x492559F64E Jun 21 '14 at 13:37
  • @eXophobia The [`EN_CHANGE`](http://msdn.microsoft.com/en-us/library/windows/desktop/bb761676.aspx) notification is sent to the **parent** window of the control, not the control itself. You will have to implement the logic in the control parent's message handler. – IInspectable Jun 21 '14 at 13:40
  • @IInspectable I see. Thank you, I never would've thought of that. That's kinda disappointing as I wanted to write my float edit control generic so I could use it in future projects. – 0x492559F64E Jun 21 '14 at 17:44
  • I don't understand the final paragraph of your edit. You refer to a comma as a colon. Very confusing. Also, why do you insist on `.` as the decimal separator. What about the majority of the world's population who use a different decimal separator? – David Heffernan Jun 23 '14 at 10:12
  • @DavidHeffernan I'm very sorry. I simply translated it from my language without looking it up. It didn't even occur to me, that "comma" can be misleading in english. – 0x492559F64E Jun 23 '14 at 10:43
  • OK, I understand now. It makes perfect sense now! – David Heffernan Jun 23 '14 at 10:46

1 Answers1

-1

You need a state machine. First declare your states.

enum InputState
{
  NoCharacters
  MinusSign
  LeadingZero
  PreDecimalPoint
  PostDecimalPoint
}
InputState mState = NoCharacters

Whenever the user inputs a character, call a different validation function depending on the value of mState using a switch block.

bool ValidCharacter(char input)
{
  switch (mState)
  {
        case NoCharacters:
          return NoCharacters(input);
        case MinusSign:
          return MinusSign(input);
        /// etc
  }
}

So for example, the function you call when mState == NoCharacters, would accept any number, decimal point or minus sign. It would then change mState to MinusSign if the character was a minus sign, LeadingZero if it was a zero, etc.

deek0146
  • 962
  • 6
  • 20