【问题标题】:How to handle VK_MENU (alt) keypresses properly using WinAPI?如何使用 WinAPI 正确处理 VK_MENU (alt) 按键?
【发布时间】:2021-05-31 21:32:34
【问题描述】:

我正在使用 C++ 上的 WinAPI (win32) 编写 Windows 应用程序,但无法弄清楚 ALT 键发生了什么。我正在处理 WM_KEYDOWN 和 WM_SYSKEYDOWN,所以所有按键都应该通过。但是,虽然检测到每个 ALT 键,但每隔一个 ALT 键就会被跳过。

每当我连续获得第二个 WM_SYSKEYUP 时,我是否必须生成自己的 keydown 事件,还是我的消息处理有问题?

详情

点击这些键:ALT ALT ALT ALT ALT(alt 五次)
预期消息序列:DUDUDUDUDU(先下后上,重复五次)
观察到的消息序列:DUUDUUDU(每一秒都不见了)

点击这些键:ALT C ALT C ALT C ALT C ALT C(在两者之间添加其他按键 - 在点击 C 之前释放 ALT)
预期的 VK_MENU 消息序列:DUDUDUDUDU(先下后上,重复五次)
观察到的 VK_MENU 消息序列:DUDUDUDUDU(如预期的那样!)

似乎添加其他按键(甚至鼠标事件)会取消任何 Windows 正在执行的操作并强制它向应用发送所有 ALT 按键。

示例代码(Visual c++17):

创建一个窗口,在按下 ALT 时将“D”添加到窗口标题,在释放 ALT 时将“U”添加到窗口标题

#ifndef UNICODE
#define UNICODE
#endif
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#define STRICT
#include <Windows.h>
#include <optional>
#include <malloc.h>

WCHAR* title;
int titleLen = 0;
const int maxTitleLen = 64;

void updateWindowTitle(WCHAR c, HWND hwnd)
{
    if (titleLen >= maxTitleLen) return;
    title[titleLen++] = c;
    title[titleLen] = 0;
    SetWindowTextW(hwnd, title);
}

LRESULT WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CLOSE:
        PostQuitMessage(0);
        return 0;
    case WM_SYSKEYDOWN: //needed for keys like alt (VK_MENU)
    case WM_KEYDOWN:
        //process_keydown_event(wParam);
        if (!(lParam & 0x40000000)) { //ignore autorepeat
            if (wParam == VK_MENU) updateWindowTitle(L'D', hwnd);
        }
        break;
    case WM_SYSKEYUP: //needed for keys like alt (VK_MENU)
    case WM_KEYUP:
        //process_keyup_event(wParam);
        if (wParam == VK_MENU) updateWindowTitle(L'U', hwnd);
        break;
    //etc...
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

// *********** STANDARD BORING SETUP BELOW ************

std::optional<int> processMessages() noexcept
{
    MSG msg;
    while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
    {
        if (msg.message == WM_QUIT)
        {
            // Return argument to PostQuitMessage as we are quitting
            return msg.wParam;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return {}; //return empty optional when not quitting
}

int CALLBACK WinMain(
    HINSTANCE hInst,
    HINSTANCE hPrevInst,
    LPSTR     lpCmdLine,
    int       nShowCmd)
{
    // Make global title point to valid memory
    title = static_cast<WCHAR*>(malloc(sizeof(WCHAR) * maxTitleLen));

    // Register window class
    WNDCLASSEX wc = { 0 };
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_OWNDC;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.hInstance = hInst;
    wc.hIcon = nullptr;
    wc.hIconSm = nullptr;
    wc.hCursor = nullptr;
    wc.hbrBackground = nullptr;
    wc.lpszMenuName = nullptr;
    wc.lpszClassName = L"my window class name";
    RegisterClassEx(&wc);

    // Create window 800x500
    HWND hwnd = CreateWindowW(
        wc.lpszClassName, L"",
        WS_VISIBLE | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
        CW_USEDEFAULT, CW_USEDEFAULT, 800, 500,
        nullptr, nullptr, hInst, nullptr
    );
    // (check HWND is not NULL)

    //main loop
    while (true)
    {
        if (const auto errorcode = processMessages())
        {
            return errorcode.value();
        }
    }
}

【问题讨论】:

  • ALT 用于许多系统命令。使用 WM_SYSKEYDOWN 通常不是一个好主意。如果您想确定是否在您的上下文中按下了 ALT,您可以使用 GetKeyState/GetAsyncKeyState stackoverflow.com/questions/1811206/… PS:您的消息泵吃 CPU,这是官方示例:docs.microsoft.com/en-us/windows/win32/winmsg/…
  • 我假设您已经确定您有正当理由覆盖默认的 SYSKEYDOWN 行为(也包括 F10)。我记得有几个关于这个的陷阱......像SDL这样的Afaik库几乎可以完成你所做的事情(它的源代码可能是一个开始寻找的好地方)。乍一看,我认为如果您致电DefWindowProc(即用return 0 替换您的break)可能会有所帮助。因为通常 VK_MENU 将应用程序置于“系统菜单模式”,所以它会对消息做出不同的响应。请尝试一下,看看它是否有效。

标签: c++ winapi


【解决方案1】:

我可以通过代码重现问题,然后通过spy++捕获窗口的消息。

我们可以看到,当第一次按下ALT键时,成功触发WM_SYSKEYDOWNWM_SETTEXT消息:

第二次按下ALT键的时候,其实是触发了WM_SYSKEYDOWN消息,但是经过一系列的消息发送(WM_CAPTURECHANGED,WM_MENUSELECT...)。这个导致消息没有在主窗口处理,而是发送到hmenu:00000000,所以主窗口没有处理WM_SYSKEYDOWN消息。

可以通过处理WM_SYSCOMMAND消息来处理:

case WM_SYSCOMMAND:
    if (wParam == SC_KEYMENU)
    {
        return 0;
    }
    else
        return DefWindowProc(hwnd, msg, wParam, lParam);
    break;

它对我有用。

【讨论】:

  • 您好,如果这个答案对您有帮助,请随时标记它以帮助遇到相同问题的人,如果您有任何问题,请告诉我。谢谢。
  • 感谢您的回答,您的解决方案绝对有效。尽管它禁用了默认行为,但在我的情况下这很好,因为我的应用程序没有或不需要系统菜单。抱歉,我花了一段时间才回复!
猜你喜欢
  • 2011-07-24
  • 2015-08-08
  • 2014-10-11
  • 1970-01-01
  • 2015-07-16
  • 2020-07-08
  • 2019-12-11
  • 2011-08-15
  • 1970-01-01
相关资源
最近更新 更多