Masked Edit Input

How to filter specific keys-strokes from a standard edit control

This article will describe one method of implementing masked edit controls. There are loads of different ways to perform the masking operation, depending on how you want to restrict the user's input, but they all revolve around the same basic principles. The method presented here will allow an edit control to mask any ANSI character, thereby restricting the input to a subset of the ANSI character set.

Masking the input to an edit control requires that we somehow intercept keystroke messages that are sent to the control whenever a key is pressed. The best way to do this is to subclass the edit control. We can let all messages go un-modified to the original window procedure, but we can handle the WM_CHAR message to suit our needs.

The Masking Function

Firstly, we will define a function which will apply a mask to an edit control.

BOOL MaskEditControl(HWND hwndEdit, char szMask[], DWORD nOptions)

This function takes three parameters.

hwndEdit is the handle to an edit control created by the current process.szMask is a text string containing the characters to mask.nOptions will specify additional masking options for the edit control. A value of TRUE means that the specified text string contains valid characters, which the edit control will accept when typed in. A value of FALSE means that the specified text string contains characters which will be rejected when input to the control.

This provides a fairly flexible way to define a mask for an edit control. For example, the following snippet will mask the specified edit control, which will subsequently only accept numeric characters, the space character and back-slashes. Including the back-slash will enable the user to delete characters from the control.

MaskEditControl(hwndEdit, "0123456789 \b", TRUE);

Masking Function Details

The function itself is pretty straight-forward. The basic steps that we will take are listed here:

  • Retrieve the pointer to current window procedure using GetWindowLong(hwnd, GWL_WNDPROC)
  • Allocate a block of memory big enough to store this function pointer, and a 256 element BOOL array.
  • Build a lookup table in this array, with each array element corresponding to one ANSI character.
  • Replace the edit control's window procedure with our custom masking one.
  • Associate our block of memory with the edit control using SetWindowLong(hwnd, GWL_USERDATA, (LONG)data).

This is basically what the function looks like:

BOOL MaskEditControl(HWND hwndEdit, char szMask[], DWORD nOptions)
{
    BOOL *mask;
    WNDPROC oldproc;
    int i;

    // don't make a new mask if there is already one available
    oldproc = (WNDPROC)GetWindowLong(hwndEdit, GWL_WNDPROC);

    if(oldproc != MaskedEditProc)
        mask = (BOOL *)HeapAlloc(GetProcessHeap(), 0, sizeof(WNDPROC) + 256 * sizeof(BOOL));
    else
        mask = (BOOL *)GetWindowLong(hwndEdit, GWL_USERDATA);

    // build the mask lookup table. The method varies depending
    // on whether we want to allow or disallow the specified szMask characters
    if(nOptions == TRUE)
    {
        for(i = 0; i < 256; i++)
            mask[i] = FALSE;

        for(i = 0; szMask[i] != '\0'; i++)
            mask[szMask[i]] = TRUE;
    }
    else
    {
        for(i = 0; i < 256; i++)
            mask[i] = TRUE;

        for(i = 0; szMask[i] != '\0'; i++)
            mask[szMask[i]] = FALSE;
    }

    //don't update the user data if it is already in place
    if(oldproc != MaskedEditProc)
    {
        mask[256] = (BOOL)GetWindowLong(hwndEdit, GWL_WNDPROC);

        SetWindowLong(hwndEdit, GWL_WNDPROC, (LONG)MaskedEditProc);
        SetWindowLong(hwndEdit, GWL_USERDATA, (LONG)mask);
    }

    return TRUE;
}

Now that we have defined a suitable interface to the masked edit control, we can go on to write a replacement window procedure.

The Masked Edit Window Procedure

LRESULT CALLBACK MaskedEditProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    WNDPROC oldwndproc;
    BOOL *mask = (BOOL * )GetWindowLong(hwnd, GWL_USERDATA);
    oldwndproc = (WNDPROC)mask[256];

    switch(uMsg)
    {
    // WM_NCDESTROY is the LAST message that a window will receive - 
    // therefore we must finally remove the old wndproc here
    case WM_NCDESTROY:
        HeapFree(GetProcessHeap(), 0, mask);
        break;

    case WM_CHAR:
        if(mask[wParam & 0xff] == TRUE)
            break;
        else
            return 0;
    }

    return CallWindowProc(oldwndproc, hwnd, uMsg, wParam, lParam);
}

That's it in a nutshell. There are lots of other ways of performing the masking operation itself. In this case I use a simple boolean lookup table, with one element corresponding to one character. The idea is the same no matter what method you choose to do the mask - subclass the edit control and associate some kind of mask with it.