cwinapivirtual-keyboardeditcontrol

Subclass edit control without ruining copy/paste


I want to create an edit control where users can only type in floating point numbers, but I want to also be able to copy/paste/cut text in this edit. So I subclassed an edit control with the following window procedure:

LRESULT CALLBACK FloatTextboxWindowProc(HWND windowHandle, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclassId, DWORD_PTR refData)
{
    switch (msg)
    {
        case WM_CHAR:
            // If the character isn't a digit or a dot, rejecting it.
            if (!(('0' <= wparam && wparam <= '9') || 
                wparam == '.' || wparam == VK_RETURN || wparam == VK_DELETE || wparam == VK_BACK))
            {
                return 0;
            }
            else if (wparam == '.') // If the digit is a dot, we want to check if there already is one.
            {
                TCHAR buffer[16];
                SendMessage(windowHandle, WM_GETTEXT, 16, (LPARAM)buffer);

                // _tcschr finds the first occurence of a character and returns NULL if it wasn't found. Rejecting this input if it found a dot.
                if (_tcschr(buffer, TEXT('.')) != NULL)
                {
                    return 0;
                }
            }

        default:
            return DefSubclassProc(windowHandle, msg, wparam, lparam);
    }
}

This works, aside from the fact that copy/paste/cut operations are blocked. Nothing happens when I try to do them.

This is confusing to me, because Microsoft says these operations are handled by WM_COPY, WM_PASTE, and WM_CUT messages, which I'm not even overriding. But I tested and found that when I type Ctrl+C, Ctrl+V, and Ctrl+X into the edit, it fires a WM_CHAR message with the key codes VK_CANCEL, VK_IME_ON, and VK_FINAL (may be respectively, I don't remember). Which is weird, because none of these keys sound like they represent these inputs, and nowhere on the Internet does anyone say they do.

If I add the condition that these key codes are passed on to DefSubclassProc() instead of being rejected, it fixes the problem. But I'm hesitant to just take this fix and move on, because I can't explain why it works, and I don't know what bugs it might introduce, resulting from what these key codes actually mean.

So, why does overriding WM_CHAR make copy/paste/cut no longer work? And why do these key codes, which seemingly have nothing to do with these inputs, get associated with them? And how might I allow copy/paste/cut in a less hacky way?


Solution

  • Per the Keyboard Input documentation on MSDN:

    Key strokes are converted into characters by the TranslateMessage function, which we first saw in Module 1. This function examines key-down messages and translates them into characters. For each character that is produced, the TranslateMessage function puts a WM_CHAR or WM_SYSCHAR message on the message queue of the window. The wParam parameter of the message contains the UTF-16 character.

    ...

    Some CTRL key combinations are translated into ASCII control characters. For example, CTRL+A is translated to the ASCII ctrl-A (SOH) character (ASCII value 0x01). For text input, you should generally filter out the control characters. Also, avoid using WM_CHAR to implement keyboard shortcuts. Instead, use WM_KEYDOWN messages; or even better, use an accelerator table. Accelerator tables are described in the next topic, Accelerator Tables.

    So, what is happening is that TranslateMessage() in your app's message loop converts WM_KEYDOWN messages for CTRL-C, CTRL-V, and CTRL-X sequences into WM_CHAR messages carrying the ASCII control characters 0x03 (ASCII ETX, aka ^C), 0x16 (ASCII SYN, aka ^V), and 0x18 (ASCII CAN, aka ^X), respectively.

    WM_CHAR carries translated character codes, NOT virtual key codes, which is why VK_CANCEL (0x03), VK_IME_ON (0x16), and VK_FINAL (0x18) are confusing you. Virtual key codes are not used in WM_CHAR. The reason why VK_RETURN and VK_BACK (but not VK_DELETE) "work" in your filtering is because those keys are translated into ASCII control characters, per the Using Keyboard Input documentation:

    A window procedure receives a character message when the TranslateMessage function translates a virtual-key code corresponding to a character key. The character messages are WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, and WM_SYSDEADCHAR. A typical window procedure ignores all character messages except WM_CHAR. The TranslateMessage function generates a WM_CHAR message when the user presses any of the following keys:

    • Any character key
    • BACKSPACE
    • ENTER (carriage return)
    • ESC
    • SHIFT+ENTER (linefeed)
    • TAB

    ENTER is translated into ASCII control character 0x0D (ASCII CR, aka ^M), which is the same numeric value as VK_RETURN.

    BACKSPACE is translated into ASCII control character 0x08 (ASCII BS, aka ^H), which is the same numeric value as VK_BACK.

    Note that the DELETE key is not on the list of translated keys, so the standard DELETE key will not generate a WM_CHAR message, as there is no ASCII control character for delete (however, the DEL (.) key on a numeric keyboard may generate a WM_CHAR message carrying VK_DELETE. In this case, bit 24 of the lParam will be 1).

    So, DefWindowProc() would translate these special WM_CHAR messages for your clipboard operations into WM_COPY, WM_PASTE, and WM_CUT messages, respectively. However, you are filtering out those messages so they don't reach DefSubclassProc(), and thus do not reach DefWindowProc().

    So, as you already discovered, you do need to allow those messages to pass through your filtering, eg:

    LRESULT CALLBACK FloatTextboxWindowProc(HWND windowHandle, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclassId, DWORD_PTR refData)
    {
        if (msg == WM_CHAR)
        {
            // If the character isn't a digit or a dot, rejecting it.
            if (!(
                (wparam >= '0' && wparam <= '9') || 
                wparam == '.' ||
                wparam == VK_RETURN ||
                wparam == VK_DELETE ||
                wparam == VK_BACK ||
                wparam == 0x03 || // CTRL-C
                wparam == 0x16 || // CTRL-V
                wparam == 0x18)   // CTRL-X
            )
            {
                return 0;
            }
            if (wparam == '.') // If the digit is a dot, we want to check if there already is one.
            {
                TCHAR buffer[16];
                SendMessage(windowHandle, WM_GETTEXT, 16, (LPARAM)buffer);
    
                // _tcschr finds the first occurence of a character and returns NULL if it wasn't found. Rejecting this input if it found a dot.
                if (_tcschr(buffer, TEXT('.')) != NULL)
                {
                    return 0;
                }
            }
        }
    
        return DefSubclassProc(windowHandle, msg, wparam, lparam);
    }