代码在有断点的情况下运行良好,但没有断点则不然

Code runs fine with breakpoints but NOT without them

LRESULT CALLBACK ProcessMsgs(HWND hwnd, UINT msg, WPARAM param, LPARAM lparam) {
    switch (msg) {
    case WM_HOTKEY: {
        WORD AUXKEY = LOWORD(lparam);
        WORD MKEY = HIWORD(lparam);
        INPUT ip;
        ip.type = INPUT_KEYBOARD;
        ip.ki.time = 0;
        ip.ki.dwExtraInfo = 0;
        ip.ki.dwFlags = KEYEVENTF_UNICODE;
        ip.ki.wVk = 0; // 0 because of unicode
        if (AUXKEY == MOD_ALT) {
            switch (MKEY) { // Lowercase
            case 0x41: // A
                ip.ki.wScan = 0xE1; // lowercase a with accent
                break;
            case 0x4E: // N
                ip.ki.wScan = 0xF1; // lowercase n with tilde accent
                break;
            case 0x4F: // O
                ip.ki.wScan = 0xF3; // lowercase o with accent
                break;
            case 0x55: // U
                ip.ki.wScan = 0xFA; // lowercase u with accent
                break;
            case 0x49: // I
                ip.ki.wScan = 0xED; // lowercase i with accent
                break;
            case 0xBD: // DASH
                ip.ki.wScan = 0x2014; // em dash
                break;
            }
            SendInput(1, &ip, sizeof(INPUT)); // breakpoint here
        }
        if (AUXKEY == MOD_ALT + MOD_SHIFT) {
            switch (MKEY) { // Uppercase
            case 0x41: // A
                ip.ki.wScan = 0xC1; // uppercase a with accent
                break;
            case 0x4E: // N
                ip.ki.wScan = 0xF1; // uppercase n with tilde accent
                break;
            case 0x4F: // O
                ip.ki.wScan = 0xD2; // uppercase o with accent
                break;
            case 0x55: // U
                ip.ki.wScan = 0xDA; // uppercase u with accent
                break;
            case 0x49: // I
                ip.ki.wScan = 0xCD; // uppercase i with accent
                break;
            }
            SendInput(1, &ip, sizeof(INPUT));

        }
        ip.ki.dwFlags = KEYEVENTF_KEYUP + KEYEVENTF_UNICODE; // KEYEVENTF_KEYUP for key release
        SendInput(1, &ip, sizeof(INPUT));
        return 0;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hwnd, msg, param, lparam);
    }
}

当与 ALT 键结合使用时,此代码会键入一个带有重音符号的元音字母。它从这样的注册热键接收消息 WM_HOTKEY

    RegisterHotKey(
        TheWindow,
        NULL,
        MOD_ALT,
        0x41 // A
    );

所有元音字母、字母 N 和破折号都有热键。我遇到的问题是当我在初始 SendInput 的行上使用断点时代码工作正常,但是当我 运行 它正常时,没有键入带重音的字母。所有这些中断都在那里,因为 switch 总是使用最后一个案例,我不知道为什么。

问题是当我 运行 我在上述代码之前开发的这个代码的 alpha 和原始版本时,它不需要断点就可以正常工作(这个版本使用 CTRL 作为指示器,我改变了它到 ALT 因为有太多的程序功能使用 CTRL 加上用户功能的字母...... RegisterHotKey 功能相应地使用 MOD_CONTROL 而不是 MOD_ALT):

switch (msg) {
    case WM_HOTKEY:
        INPUT ip;
        ip.type = INPUT_KEYBOARD;
        ip.ki.wScan = 0xE1; // lowercase a with accent
        ip.ki.time = 0;
        ip.ki.dwExtraInfo = 0;
        ip.ki.dwFlags = KEYEVENTF_UNICODE;
        ip.ki.wVk = 0; // 0 because of unicode
        SendInput(1, &ip, sizeof(INPUT));

        ip.ki.dwFlags = KEYEVENTF_KEYUP | KEYEVENTF_UNICODE; // KEYEVENTF_KEYUP for key release
        SendInput(1, &ip, sizeof(INPUT));
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hwnd, msg, param, lparam);
    }

我知道我可以使用标识符来简化其中的一些,但无论如何......代码功能,只是带有断点......太奇怪了。

问题

不幸的是,我手头没有 Windows 系统来测试,但我想我可以猜出你的问题是什么 - 但是很好的 heisenbug,我花了很长时间才弄明白 :D

请记住,即使您注册了热键,目标 window 仍会获得按键。
除此之外 SendInput will not reset the keyboard state:

This function does not reset the keyboard's current state. Any keys that are already pressed when the function is called might interfere with the events that this function generates. To avoid this problem, check the keyboard's state with the GetAsyncKeyState function and correct as necessary.

因此,从目标 window 的角度来看,您会看到以下关键事件:

  • ALT向下
  • A向下
  • (热键激活)
  • á向下
  • á向上
  • (用户需要一些时间来释放按键)
  • A向上
  • ALT向上

而如果您正在调试,您可能会在遇到断点后释放按键,因此序列将如下所示:

  • ALT向下
  • A向下
  • (热键激活)
  • (断点命中)
  • (用户释放按键)
  • A向上
  • ALT向上
  • (恢复进程)
  • á向下
  • á向上

所以这就是它在您调试时起作用的最可能原因 - 一旦遇到断点,您就会释放按键。

ALT 键本身有点邪恶 - 主要是因为只要您按住 ALT 键,window 将不会 得到 WM_KEYDOWN 条消息,但只有 WM_SYSKEYDOWN 条消息:Key-Down and Key-Up Messages

The WM_SYSKEYDOWN message indicates a system key, which is a key stroke that invokes a system command. There are two types of system key:

  • ALT + any key
  • F10

All other key strokes are considered nonsystem keys and produce the WM_KEYDOWN message. This includes the function keys other than F10.

所以基本上目标应用程序将完全忽略您的输入,因为它仅将其作为 SYSKEY 消息接收。


解决方案

您可以通过检查在调用热键时 ALT 键(或任何其他修改键)是否仍然按下来解决此问题,例如通过 GetAsyncKeyState(),然后使用该修改键的释放事件调用 SendInput(),然后发送您的特殊字符,然后恢复修改键的状态。

此外,我建议将所有已发送的按键事件集中到一个 SendInput() 中,以确保您的输入事件之间没有交错的用户输入(或其他程序输入)。

例如:

std::vector<INPUT> inputEvents;

short altKeyState = GetAsyncKeyState(VK_MENU);
// if we can't get the current input state
// we most likely aren't allowed to inject input
if(altKeyState == 0) return 0;
bool isAltDown = altKeyState < 0;
if(isAltDown) {
    INPUT ip;
    ip.type = INPUT_KEYBOARD;
    ip.ki.time = 0;
    ip.ki.dwExtraInfo = 0;
    ip.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
    ip.ki.wVk = VK_MENU;
    inputEvents.push_back(ip);
}
// TODO: Add other modifier keys that might interfere with input

// Generate your char input
INPUT ips;
ips.type = INPUT_KEYBOARD;
ips.ki.wScan = 0xE1;
ips.ki.time = 0;
ips.ki.dwExtraInfo = 0;
ips.ki.dwFlags = KEYEVENTF_UNICODE;
ips.ki.wVk = 0;
inputEvents.push_back(ips);

if(isAltDown) {
    INPUT ip;
    ip.type = INPUT_KEYBOARD;
    ip.ki.time = 0;
    ip.ki.dwExtraInfo = 0;
    ip.ki.dwFlags = KEYEVENTF_SCANCODE;
    ip.ki.wVk = VK_MENU;
    inputEvents.push_back(ip);
}
// TODO: Add other modifier keys that might interfere with input


// Submit all inputs to the queue
UINT result = SendInput(inputEvents.size(), &inputEvents[0], sizeof(INPUT));
if(result == inputEvents.size()) {
    // SUCCESS! Injected all inputs
} else if (result > 0) {
   // Partial success, something failed, not really much you can do here other than call GetLastError()
} else {
  // blocked by other thread
}

请注意,您的程序仍受制于 UIPI(用户界面特权隔离),因此您不能使用 SendInput() 将符号注入程序 运行 更高的完整性级别。

如果您关注 window 具有更高完整性级别的程序,它根本不会收到您的 SendInput() 消息 - 但您也无法检测到完全失败。
(除非它使用 ChangeWindowMessageFilter(Ex) 允许来自低优先级进程的键盘输入)