ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [WinAPI] 키보드 입력 (WM_KEYDOWN, WM_KEYUP, GetAsyncKeyState)
    C++/미분류 2024. 2. 23. 14:56

    Windows프로그램 개발 시, 키보드 입력을 처리하는 방법은 상황에 따라 여러 함수를 이용하는 것으로 나뉘게 됩니다.

    이와 관련한 방법들이 MSDN에 소개되어 있습니다.

    Keyboard Input - Win32 apps | Microsoft Learn

    이번 글에서는 여러 방법 중 Windows 메시지의 KEYDOWN, KEYUP 메시지를 처리하는 방법과, GetAsyncKeyState함수를 이용하는 방법을 살펴보도록 하겠습니다.

     

    이번 글은 이전 글([WinAPI] 로그 출력하기 (Console, TRACE))의 예제를 활용하고 있습니다.

     


     

    1. WM_KEYDOWN, WM_KEYUP

     

    더보기

    각각 키 입력이 발생했을 때, 해제되었을 때 전달되는 메시지 입니다.

    윈도우 프로시저에서 다음과 같은 방법으로 처리가 가능합니다.

    LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
        ...
        switch(message) {
        case WM_KEYDOWN:
            switch(wParam) {
            case 'A':
                std::cout << "Input A\n";
                break;
            case VK_CONTROL:
                std::cout << "Input Ctrl\n";
                break;
            }
            break;
        case WM_KEYUP:
            switch(wParam) {
            case 'A':
                std::cout << "Release A\n";
                break;
            case VK_CONTROL:
                std::cout << "Release Ctrl\n";
                break;
            }
            break;
        ...
        }
        ...
    }

    위 예제는 A키와 Ctrl키의 입력과 해제에 대해 메시지를 출력하는 코드입니다.

     

    WM_KEYDOWN및 KEYUP메시지에서 WPARAM은 입력된 키에 대한 값을 의미합니다.

    키 값은 16진수로 정의되어 있습니다.

    특이 사항으로, 방향키 및 기능 키(Ctrl, Insert, PageUp ... etc)는 특정한 상수로 정의되어 있습니다.

    정의되어 있는 상수 및 키 값은 MSDN에 정리되어 있습니다.

    Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn

     

     

     

    2. WM_KEYDOWN, KEYUP의 혼용 (lParam)

     

    더보기

    1번 문단의 코드를 이용할 경우, 동일한 키 입력이더라도 KEYDOWN, KEYUP에 대해 따로 처리를 해야 합니다.

    이번 문단에서는 1번 문단의 코드를 조금 더 간결하게 사용하는 방법을 소개합니다.

     

    WM_KEYDOWN, KEYUP메시지는 lParam으로 메시지에 대한 상세 정보가 전달됩니다.

    메시지에 대한 상세 정보는 MSDN에 정리되어 있습니다.

    WM_KEYDOWN message (Winuser.h) - Win32 apps | Microsoft Learn
    WM_KEYUP message (Winuser.h) - Win32 apps | Microsoft Learn

    lParam은 32비트의 데이터로, 각 비트별로 특정한 값을 의미합니다.

    그 중, 이번 문단에서 살펴볼 비트는 31번째 비트입니다.

    해당 비트는 WM_KEYDOWN의 경우 항상 0으로, WM_KEYUP의 경우는 항상 1의 값을 가집니다.

    위 특징을 참고하여 1번 문단의 예제 코드를 수정할 경우 다음과 같이 작성할 수 있습니다.

    LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
        ...
        switch(message) {
        case WM_KEYDOWN:
        case WM_KEYUP
            switch(wParam) {
            case 'A':
                if(lParam & 0x80000000) {
                    std::cout << "Input A\n";
                } else {
                    std::cout << "Release A\n";
                }
                break;
            case VK_CONTROL:
                if(lParam & 0x80000000) {
                    std::cout << "Input Ctrl\n";
                } else {
                    std::cout << "Release Ctrl\n";
                }
                break;
            }
            break;
        ...
        }
        ...
    }

    기능은 동일하지만, 한 종류의 키에 대해 입력과 해제를 동시에 확인할 수 있습니다. 

     

    3. GetAsyncKeyState

     

    더보기

    위 문단의 KEYDOWN, KEYUP메시지는 메시지 큐를 통해 동기적으로 작동합니다.

    즉, 한 번에 한 개의 키에 대한 메시지만 전달됩니다.

    따라서 연속적인 입력에 대한 처리는 가능하지만, 키가 동시에 입력되는 경우에 대한 처리는 불가능합니다.

     

    이번에 소개할 GetAsyncKeyState함수는 위 문제에서 벗어난 함수입니다.

    GetAsyncKeyState는 메시지 큐를 거치지 않고 비동기로 키 입력을 감지합니다.

    GetAsyncKeyState함수에 대한 내용은 MSDN에 정리되어 있습니다.

    GetAsyncKeyState function (winuser.h) - Win32 apps | Microsoft Learn

    함수는 다음과 같이 정의되어 있습니다.

    SHORT GetAsyncKeyState(int vKey);

    입력은 가상 키 코드입니다. 이전 문단에서 확인한 사용한 입력 키와 동일합니다. 

    Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn

    출력 타입은 SHORT자료형으로 정의되어 있지만, 출력 값은 MSB와 LSB만 변화하며 나머지 비트는 0입니다.

    MSB는 입력으로 전달한 키가 눌렸는지의 여부를 반환합니다. 눌렸으면 1, 눌리지 않았으면 0입니다.

    LSB는 이전 GetAsyncKeyState함수 호출 시점에 키가 눌렸는지의 여부에 따라 달라집니다. 이전 함수 호출 시 키가 눌러져 있었을 경우 1, 눌리지 않았을 경우 0입니다.

    즉, MSB는 현재 시점의 키 입력 여부, LSB는 이전 시점의 키 입력 여부를 표현합니다.

    하지만 MSDN문서는 LSB에 의존하지 않는 것을 권합니다.

    However, you should not rely on this last behavior; for more information, see the Remarks.

    The return value is zero for the following cases:
    - The current desktop is not the active desktop
    - The foreground thread belongs to another process and the desktop does not allow the hook or the journal record.

    MSDN의 출력 타입을 설명하는 부분의 일부분으로, LSB가 특정 경우에 0이 될 수 있는 경우를 설명하고 있습니다.

    요약하면, 하드웨어나 운영체제의 상태에 따라 LSB가 0이 될 수 있다는 내용입니다.

     

    따라서, MSB만 이용해서 입력과 해제를 다루도록 하겠습니다.

    우선 필요한 함수와 변수를 클래스에 추가로 선언합니다.

    ...
    #include <map>
    
    class MyApp {
    private:
        void HandleKeyboardInput();
        ...
    private:
        std::map<DWORD, bool> inputFlag;
        ...
    };

    HandleKeyboardInput함수는 입력을 처리할 함수입니다.

    이 함수는 메시지 루프에서 호출됩니다.

    변수로 선언한 inputFlag는 입력 해제를 감지할 bool변수입니다.

    여러 입력 키에 대응할 수 있도록 map으로 선언했습니다.

     

    HandleKeyboardInput함수는 다음과 같이 구현합니다.

    void MyApp::HandleKeyboardInput() {
        if (GetAsyncKeyState('A') & 0x8000) {
            inputFlag['A'] = true;
            std::cout << "Input A\n";
        } else {
            if (inputFlag['A']) {
                inputFlag['A'] = false;
                std::cout << "Release A\n";
            }
        }
    }

    GetAsyncKeyState함수의 출력을 비트 연산을 통해 입력을 감지합니다.

    입력이 없을 경우, inputFlag의 상태에 따라 동작이 달라집니다.

    inputFlag가 true가 되었을 경우는 입력이 있을 경우이므로, 입력이 없으며 inputFlag가 true일 때 입력이 해제됨을 감지할 수 있습니다.

     

    위와 같이 구현한 HandleKeyboardInput함수는 메시지 루프에서 호출됩니다.

    void MyApp::runMessageLoop() {
        MSG msg;
    
        BOOL bRet;
        while ((bRet = GetMessage(&msg, nullptr, 0, 0)) != 0) {
            if (bRet == -1) {
                // Error handling
            } else {
                HandleKeyboardInput();
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
    }

    HandleKeyboardInput은 메시지 루프와 무관한 입력을 처리하는 함수입니다.

    따라서 메시지가 송수신과 무관하므로 윈도우 프로시저에서 호출할 필요가 없습니다.

     

    4. GetAsyncKeyState를 통한 멀티 입력 감지

     

    더보기

    GetAsyncKeyState를 통해 다중 입력을 감지하는 코드입니다.

    void MyApp::HandleKeyboardInput() {
        bool isCtrl = GetAsyncKeyState(VK_CONTROL) & 0x8000;
        if (isCtrl && (GetAsyncKeyState('D') & 0x8000)) {
            std::cout << "Ctrl D\n";
        }
        ...
    }

    위 예제는 Ctrl + D의 입력을 감지하는 코드입니다. 

    Ctrl, Shift, Alt등의 기능 키는 다른 키와 조합되는 경우가 많기 때문에, 조건문 바깥에 변수로 선언했습니다.

    위 코드에 Ctrl키와 다른 키가 조합되어야 할 경우, 코드가 간결해질 수 있습니다.

     


     

    키보드 입력을 다루는 방법 두 가지를 살펴보았습니다.

    예제를 실행해보면, 두 방법의 로그 출력 속도가 다른 것을 볼 수 있습니다.

    이는 프로그램의 UX와도 연결되는 부분이므로, 만드는 프로그램에 알맞는 방법을 취하는 것이 좋겠습니다.

     

    이번 글에서 사용된 예제의 전체 코드는 Github에서 확인하실 수 있습니다.

     

    GitHub - ruru14/WinapiStudy

    Contribute to ruru14/WinapiStudy development by creating an account on GitHub.

    github.com

     

    이번 글이 도움이 되셨기를 바랍니다.

    감사합니다.

    댓글

Designed by Tistory.