不破坏子类编辑控件 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_COPYWM_PASTEWM_CUT 消息处理的,我什至没有覆盖这些消息。但是我测试发现,当我输入Ctrl+CCtrl+VCtrl+X[=34=时] 进入编辑,它会触发一条 WM_CHAR 消息,其中包含键码 VK_CANCELVK_IME_ONVK_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-CCTRL-VCTRL-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_COPYWM_PASTEWM_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);
}