不破坏子类编辑控件 copy/paste
Subclass edit control without ruining copy/paste
我想创建一个编辑控件,用户只能在其中输入浮点数,但我还希望能够在此编辑中 copy/paste/cut 文本。因此,我使用以下 window 过程对编辑控件进行子类化:
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);
}
}
除了 copy/paste/cut 操作被阻止之外,这有效。当我尝试这样做时没有任何反应。
这让我感到困惑,因为微软说这些操作是由 WM_COPY
、WM_PASTE
和 WM_CUT
消息处理的,我什至没有覆盖这些消息。但是我测试发现,当我输入Ctrl+C、Ctrl+V和Ctrl+X[=34=时] 进入编辑,它会触发一条 WM_CHAR
消息,其中包含键码 VK_CANCEL
、VK_IME_ON
和 VK_FINAL
(可能是分别的,我不记得了)。这很奇怪,因为这些键中的 none 听起来像是代表这些输入,而在 Internet 上没有任何人说它们代表这些输入。
如果我添加这些键码被传递给 DefSubclassProc()
而不是被拒绝的条件,它就解决了问题。但我对是否接受此修复并继续前进犹豫不决,因为我无法解释它为何起作用,而且我不知道它可能会引入哪些错误,这些错误是由这些关键代码的实际含义造成的。
那么,为什么覆盖 WM_CHAR
会使 copy/paste/cut 不再有效?为什么这些看似与这些输入无关的关键代码会与它们相关联?我怎样才能让 copy/paste/cut 以一种不那么古怪的方式出现?
根据 MSDN 上的 Keyboard Input 文档:
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.
所以,发生的事情是应用消息循环中的 TranslateMessage()
将 WM_KEYDOWN
消息转换为 CTRL-C、CTRL-V 和 CTRL-X 序列到携带 ASCII 控制字符 [=93= 的 WM_CHAR
消息中] 0x03(ASCII ETX
,又名 ^C
),0x16(ASCII SYN
,又名 ^V
)和 0x18(ASCII CAN
,又名 ^X
), 分别.
WM_CHAR
携带 翻译的字符代码 ,而不是 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 文档:
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 被翻译成 ASCII 控制字符 0x0D(ASCII CR
,又名 ^M
),它与 [=31= 的数值相同].
BACKSPACE被翻译成ASCII控制字符0x08(ASCII BS
,又名^H
),与[=32=是相同的数值].
请注意 DELETE 键不在翻译键列表中,因此标准 DELETE 键不会生成 WM_CHAR
消息,因为没有用于删除的 ASCII 控制字符(但是,数字键盘上的 DEL (.) 键可能会生成一条 WM_CHAR
消息,其中包含 VK_DELETE
。在这种情况下,lParam
的第 24 位将为 1).
因此,DefWindowProc()
会将这些用于剪贴板操作的特殊 WM_CHAR
消息分别转换为 WM_COPY
、WM_PASTE
和 WM_CUT
消息。但是,您正在过滤掉这些消息,因此它们不会到达 DefSubclassProc()
,因此不会到达 DefWindowProc()
.
因此,正如您已经发现的那样,您确实需要允许这些消息通过您的过滤,例如:
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);
}
我想创建一个编辑控件,用户只能在其中输入浮点数,但我还希望能够在此编辑中 copy/paste/cut 文本。因此,我使用以下 window 过程对编辑控件进行子类化:
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);
}
}
除了 copy/paste/cut 操作被阻止之外,这有效。当我尝试这样做时没有任何反应。
这让我感到困惑,因为微软说这些操作是由 WM_COPY
、WM_PASTE
和 WM_CUT
消息处理的,我什至没有覆盖这些消息。但是我测试发现,当我输入Ctrl+C、Ctrl+V和Ctrl+X[=34=时] 进入编辑,它会触发一条 WM_CHAR
消息,其中包含键码 VK_CANCEL
、VK_IME_ON
和 VK_FINAL
(可能是分别的,我不记得了)。这很奇怪,因为这些键中的 none 听起来像是代表这些输入,而在 Internet 上没有任何人说它们代表这些输入。
如果我添加这些键码被传递给 DefSubclassProc()
而不是被拒绝的条件,它就解决了问题。但我对是否接受此修复并继续前进犹豫不决,因为我无法解释它为何起作用,而且我不知道它可能会引入哪些错误,这些错误是由这些关键代码的实际含义造成的。
那么,为什么覆盖 WM_CHAR
会使 copy/paste/cut 不再有效?为什么这些看似与这些输入无关的关键代码会与它们相关联?我怎样才能让 copy/paste/cut 以一种不那么古怪的方式出现?
根据 MSDN 上的 Keyboard Input 文档:
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.
所以,发生的事情是应用消息循环中的 TranslateMessage()
将 WM_KEYDOWN
消息转换为 CTRL-C、CTRL-V 和 CTRL-X 序列到携带 ASCII 控制字符 [=93= 的 WM_CHAR
消息中] 0x03(ASCII ETX
,又名 ^C
),0x16(ASCII SYN
,又名 ^V
)和 0x18(ASCII CAN
,又名 ^X
), 分别.
WM_CHAR
携带 翻译的字符代码 ,而不是 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 文档:
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 被翻译成 ASCII 控制字符 0x0D(ASCII CR
,又名 ^M
),它与 [=31= 的数值相同].
BACKSPACE被翻译成ASCII控制字符0x08(ASCII BS
,又名^H
),与[=32=是相同的数值].
请注意 DELETE 键不在翻译键列表中,因此标准 DELETE 键不会生成 WM_CHAR
消息,因为没有用于删除的 ASCII 控制字符(但是,数字键盘上的 DEL (.) 键可能会生成一条 WM_CHAR
消息,其中包含 VK_DELETE
。在这种情况下,lParam
的第 24 位将为 1).
因此,DefWindowProc()
会将这些用于剪贴板操作的特殊 WM_CHAR
消息分别转换为 WM_COPY
、WM_PASTE
和 WM_CUT
消息。但是,您正在过滤掉这些消息,因此它们不会到达 DefSubclassProc()
,因此不会到达 DefWindowProc()
.
因此,正如您已经发现的那样,您确实需要允许这些消息通过您的过滤,例如:
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);
}