不破坏复制/粘贴的子类编辑控件

Posted

技术标签:

【中文标题】不破坏复制/粘贴的子类编辑控件【英文标题】:Subclass edit control without ruining copy/paste 【发布时间】:2020-08-22 00:30:54 【问题描述】:

我想创建一个编辑控件,用户只能在其中输入浮点数,但我还希望能够在此编辑中复制/粘贴/剪切文本。因此,我使用以下窗口过程对编辑控件进行了子类化:

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);
    

除了复制/粘贴/剪切操作被阻止之外,这很有效。当我尝试这样做时,什么也没有发生。

这让我很困惑,因为微软说这些操作由WM_COPYWM_PASTEWM_CUT 消息处理,我什至没有覆盖这些消息。但我测试并发现当我在编辑中键入 Ctrl+CCtrl+VCtrl+X 时,它会触发一个 @ 987654325@ 带有密钥代码VK_CANCELVK_IME_ONVK_FINAL 的消息(可能分别是,我不记得了)。这很奇怪,因为这些键听起来都不像代表这些输入,而且互联网上也没有人说它们代表这些输入。

如果我添加将这些密钥代码传递给DefSubclassProc() 而不是被拒绝的条件,则可以解决问题。但是我很犹豫是否接受这个修复并继续前进,因为我无法解释它为什么会起作用,而且我不知道它可能会引入什么错误,这是由这些关键代码的实际含义引起的。

那么,为什么覆盖 WM_CHAR 会使复制/粘贴/剪切不再起作用?为什么这些看似与这些输入无关的关键代码会与它们相关联?以及如何允许以不那么老套的方式进行复制/粘贴/剪切?

【问题讨论】:

如果你想阻止他们粘贴无效字符,不要忘记处理WM_PASTE 【参考方案1】:

根据 MSDN 上的 Keyboard Input 文档:

击键通过TranslateMessage 函数转换为字符,我们在模块 1 中首次看到该函数。该函数检查按键消息并将其转换为字符。 对于产生的每个字符,TranslateMessage 函数将WM_CHARWM_SYSCHAR 消息放入窗口的消息队列中。 消息的wParam 参数包含 UTF-16 字符。

...

某些 CTRL 键组合被转换为 ASCII 控制字符。例如,CTRL+A 被转换为 ASCII ctrl-A (SOH) 字符(ASCII 值 0x01)。对于文本输入,您通常应该过滤掉控制字符。另外,避免使用WM_CHAR 来实现键盘快捷键。相反,使用WM_KEYDOWN 消息;甚至更好的是,使用加速器表。下一个主题Accelerator Tables 中描述了加速器表。

所以,发生的事情是应用程序消息循环中的 TranslateMessage()WM_KEYDOWN 消息转换为 CTRL-CCTRL-V CTRL-X 序列化为带有 ASCII 控制字符 0x03(ASCII ETX,又名^C)、0x16(ASCII SYN,又名^V)的WM_CHAR 消息, 和 0x18 (ASCII CAN, aka ^X)。

WM_CHAR 带有翻译的字符代码,而不是 virtual key codes,这就是为什么 VK_CANCEL (0x03)、VK_IME_ON (0x16) 和 VK_FINAL (0x18) 会让您感到困惑. WM_CHAR 中不使用虚拟键码。 VK_RETURNVK_BACK(但不是 VK_DELETE)在您的过滤中“起作用”的原因是因为根据 Using Keyboard Input 文档,这些键被翻译为 ASCII 控制字符:

TranslateMessage 函数转换对应于字符键的虚拟键代码时,窗口过程会收到字符消息。字符消息是WM_CHARWM_DEADCHARWM_SYSCHARWM_SYSDEADCHAR。典型的窗口过程会忽略除WM_CHAR 之外的所有字符消息。 当用户按下以下任意键时,TranslateMessage 函数会生成 WM_CHAR 消息

任意字符键 退格键 ENTER(回车) ESC SHIFT+ENTER(换行) 标签

ENTER 被翻译成 ASCII 控制字符 0x0D(ASCII CR,又名^M),与VK_RETURN 是相同的数值。

BACKSPACE 被翻译成 ASCII 控制字符 0x08(ASCII BS,又名^H),与VK_BACK 是相同的数值。

注意 DELETE 键不在已翻译键列表中,因此标准 DELETE 键不会生成WM_CHAR 消息,因为没有 ASCII用于删除的控制字符(但是,数字键盘上的 DEL (.) 键可能会生成带有 VK_DELETEWM_CHAR 消息。在这种情况下,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);

【讨论】:

以上是关于不破坏复制/粘贴的子类编辑控件的主要内容,如果未能解决你的问题,请参考以下文章

求一文本编辑器控件

在 UITextField 上启用复制和粘贴而不使其可编辑

拦截粘贴到(丰富的)编辑控件

如何修改CEdit控件的上下文菜单?

matlab gui粘贴

在自定义视图/uiview 子类上实现 iphone 的复制/粘贴控件