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?
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, theTranslateMessage
function puts aWM_CHAR
orWM_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, useWM_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 areWM_CHAR
,WM_DEADCHAR
,WM_SYSCHAR
, andWM_SYSDEADCHAR
. A typical window procedure ignores all character messages exceptWM_CHAR
. TheTranslateMessage
function generates aWM_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);
}