웬디의 기묘한 이야기

글 작성자: WENDYS
반응형

마우스 후킹을 이용한 매크로 프로그램 제작

저번 포스팅에 이어 이번에도 마우스 후킹에 관련된 내용입니다.

아직 못 보셨다면 기본 마우스 후킹에 대한 내용을 보시고 보셔도 좋습니다.

https://wendys.tistory.com/110

 

[C/C++] 윈도우 마우스 후킹으로 제스처 인식 프로그램 만들기 (마우스 이동 방향 및 각도 구하기)

마우스 후킹을 이용한 매크로 프로그램 제작 저번 포스팅에 이어 이번에도 마우스 후킹에 관련된 내용입니다. 아직 못 보셨다면 기본 마우스 후킹에 대한 내용을 보시고 보셔도 좋습니다. https://wendys.tistory...

wendys.tistory.com

드디어 마무리 시간입니다.

이제 마우스의 이동 거리의 계산을 통한 방향 확인이 가능해진 시점에서 해야 할 일은 이동 방향의 목록을 만들어서 특정 기능을 할 수 있도록 하는 것만 남았습니다. 저번 시간에 80%까지 마무리 후 직접 해보는 걸 권장해드렸지만 이렇게 찾아주셨는데 마무리까지 책임져 드리는 게 좋을 것 같아서 진행하겠습니다.

 

 

마지막으로 제시해드렸던 enum 은 보기가 조금 더 편한 거지 선호하시는 방법으로 하시길 추천드립니다.

enum mouse_gesture : long {
    unknown = 0,
    left,
    right,
    up,
    down
};

목록을 사용해야 하니 가장 간편한 std::list를 사용할 예정입니다. std::vector를 사용해도 되고, 어떤 걸 사용하던지 상관없습니다. 다만 List를 접근할 때는 전 보통 Lock Object를 사용합니다. 이유는 멀티 스레드 환경에서 1번 스레드가 List에 접근하여 값을 변경하고있는데, 이 때 2번 쓰레드가 그 값을 확인하려거나 동시에 변경을 시도하는 경우 원하지 않은 결과가 나오기 때문입니다. 그렇기 때문에 개발 시 List를 사용할 때엔 습관적으로 Lock Object를 사용하는 것이 좋습니다.

#include <mutex>
#include <list>

class hook_mouse_callback {
...
private:
...
    std::list<mouse_gesture> _gesture_list;
    std::mutex _gesture_list_lock;
};

List를 사용하기 위해서 멤버 변수로 _gesture_list를 추가하고, List접근 시 동기화를 위하여 Lock Object를 선언합니다. Lock Object는 CriticalSection을 사용해도 되고 std::mutex를 사용해도 되고 동기화 객체는 무엇이든 상관없습니다. 편한 걸로 사용하시되 주의하실 점은 동기화 오브젝트 사용 중 데드락이 걸리지 않도록 꼭 관리해주셔야 합니다.

 

동기화 오브젝트 사용 시 RAII라는 디자인 패턴을 주로 사용하며, Auto Lock 형태로써 지역 스코프가 완료되는 시점에 자동으로 해제를 하도록 하여 해제를 누락시키지 않는 기법이 있으니 참고하시면 좋습니다.

https://wendys.tistory.com/11

 

[C++] RAII 패턴 - Resource Acquisition Is Initialization Pattern

RAII RAII(Resource Acquisition Is Initialization)은 유명한 design pattern 중의 하나로 C++ 언어의 창시자인 Bjarne Stroustrup에 의해 제안되었다. RAII 패턴은 C++ 같이 개발자가 직접 resource 관리를 해주..

wendys.tistory.com

위와 같이 추가했으면 준비는 다 되었으니 본론으로 돌아와서 바로 코드로 보겠습니다.

if (wparam == WM_RBUTTONDOWN) {
    _is_right_button_down = true;

    //
    // mouse gesture clear.
    //

    _gesture_list.clear();

    _latest_mouse_point = mouse_param->pt;
}
else if (wparam == WM_MOUSEMOVE) {

    if (_is_right_button_down) {

        POINT current_point = mouse_param->pt;

        double distance = get_distance(_latest_mouse_point, current_point);
        if (distance < __min_distance) {
            break;
        }

        //
        // 방향 결정 ← ↑ ↓ → 목록 추가
        //

        double dx = _latest_mouse_point.x - current_point.x;
        double dy = _latest_mouse_point.y - current_point.y;

        double radian = atan2(dy, dx);
        double degree = (radian * 180) / __pi;

        if (degree < 0.0) {
            degree = degree + 360.0;
        }

        mouse_gesture gesture = mouse_gesture::unknown;
        if (degree < 35 || degree > 315) {
            gesture = mouse_gesture::left;
        }
        else if (degree > 135) {
            if (degree > 225) {
                gesture = mouse_gesture::down;
            }
            else {
                gesture = mouse_gesture::right;
            }
        }
        else {
            gesture = mouse_gesture::up;
        }

        //
        // 해당 방향을 list에 저장 (같은 방향인경우 추가하지 않고 진행)
        //

        if (_gesture_list.empty()) {
            _gesture_list.push_back(gesture);
        }
        else {
            if (_gesture_list.back() != gesture) {
                _gesture_list.push_back(gesture);
            }
        }

        //
        // 기준 포인트를 현재 포인트로 변환
        //

        _latest_mouse_point = current_point;
    }
}

WM_RBUTTONDOWN에서 _gesture_list.clear()를 호출하고 있습니다. 마우스 우클릭 이벤트가 발생할때마다 새로운 방향 및 커맨드로 판단을 해야하기때문에 위의 타이밍에 처리되도록 되었으며, WM_RBUTTONUP 시점에 clear() 처리를 해도 문제 없습니다만 최초 실행시점에도 clear를 하기위해 WM_RBUTTONDOWN 에서 처리되었습니다.

 

목록이 잘 생성되었을까요?

그렇다면 목록이 잘 생성되었는지는 어떻게 확인하면 될까요?? WM_RBUTTONUP 시점에서 판단하여 처리를 해줘야 합니다.

else if (wparam == WM_RBUTTONUP) {
    _is_right_button_down = false;

    {
        //
        // command handling
        //

        std::wstring command;
        std::lock_guard<std::mutex> lock(_gesture_list_lock);
        if (false == _gesture_list.empty()) {
            for (const auto& gesture : _gesture_list) {
                        
                if (mouse_gesture::left == gesture) {
                    command.push_back('L');
                }
                else if (mouse_gesture::up == gesture) {
                    command.push_back('U');
                }
                else if (mouse_gesture::down == gesture) {
                    command.push_back('D');
                }
                else if (mouse_gesture::right == gesture) {
                    command.push_back('R');
                }
            }

            command.push_back('\n');
            OutputDebugString(command.c_str());
        }
    }
}

WM_MOUSEMOVE시 추가되었던 _gesture_list를 확인하여 각각 방향에 대하여 LUDR 등 커맨드를 생성할 수 있습니다.

이렇게 생성된 커맨드를 이용하면 처음에 보여드렸던 마우스 매크로 프로그램을 만들 수 있습니다.

간단하게 커맨드 처리하는 내용을 보여드리자면 다음 코드처럼 처리하실 수 있습니다.

 

완성된 전체 코드

inline double get_distance(
    __in const POINT p1,
    __in const POINT p2
    ) {
    double distance = 0.0;
    distance = sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));

    return distance;
}

inline void keyboard_input(
    __in WORD virtual_key,
    __in bool extended,
    __in bool key_down
    ) {
    INPUT input = {};
    input.type = INPUT_KEYBOARD;
    input.ki.wVk = virtual_key;
    input.ki.dwFlags = key_down ? 0 : KEYEVENTF_KEYUP;
    if (extended) {
        input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
    }
    SendInput(1, &input, sizeof(INPUT));
}

inline void keyboard_input_down(
    __in WORD virtual_key,
    __in bool extended
    ) {
    keyboard_input(virtual_key, extended, true);
}
inline void keyboard_input_up(
    __in WORD virtual_key,
    __in bool extended
    ) {
    keyboard_input(virtual_key, extended, false);
}

bool hook_mouse_callback::execute_command(
    __in std::string command
    ) {

    bool is_execute_command = false;

    if (0 == command.compare("U")) {
        keyboard_input_down(VK_CONTROL, true);
        keyboard_input_down(VK_HOME, true);

        keyboard_input_up(VK_HOME, true);
        keyboard_input_up(VK_CONTROL, true);

        is_execute_command = true;
    }
    else if(0 == command.compare("D")) {
        keyboard_input_down(VK_CONTROL, true);
        keyboard_input_down(VK_END, true);

        keyboard_input_up(VK_END, true);
        keyboard_input_up(VK_CONTROL, true);

        is_execute_command = true;
    }

    return is_execute_command;
}

LRESULT hook_mouse_callback::new_function(
    __in int code,
    __in WPARAM wparam,
    __in LPARAM lparam
    ) {

    do {
        if (code < HC_ACTION) {
            break;
        }

        MOUSEHOOKSTRUCT* mouse_param = (MOUSEHOOKSTRUCT*)lparam;

        if (wparam == WM_RBUTTONDOWN) {
            _is_right_button_down = true;

            //
            // mouse gesture clear.
            //

            _gesture_list.clear();

            _latest_mouse_point = mouse_param->pt;
        }
        else if (wparam == WM_MOUSEMOVE) {
            if (_is_right_button_down) {

                POINT current_point = mouse_param->pt;

                double distance = get_distance(_latest_mouse_point, current_point);
                if (distance < __min_distance) {
                    break;
                }

                //
                // 방향 결정 ← ↑ ↓ → 목록 추가
                //

                double dx = _latest_mouse_point.x - current_point.x;
                double dy = _latest_mouse_point.y - current_point.y;

                double radian = atan2(dy, dx);
                double degree = (radian * 180) / __pi;

                if (degree < 0.0) {
                    degree = degree + 360.0;
                }

                mouse_gesture gesture = mouse_gesture::unknown;
                if (degree < 35 || degree > 315) {
                    gesture = mouse_gesture::left;
                }
                else if (degree > 135) {
                    if (degree > 225) {
                        gesture = mouse_gesture::down;
                    }
                    else {
                        gesture = mouse_gesture::right;
                    }
                }
                else {
                    gesture = mouse_gesture::up;
                }

                //
                // 해당 방향을 list에 저장 (같은 방향인경우 추가하지 않고 진행)
                //

                if (_gesture_list.empty()) {
                    _gesture_list.push_back(gesture);
                }
                else {
                    if (_gesture_list.back() != gesture) {
                        _gesture_list.push_back(gesture);
                    }
                }

                //
                // 기준 포인트를 현재 포인트로 변환
                //

                _latest_mouse_point = current_point;
            }
        }
        else if (wparam == WM_RBUTTONUP) {
            _is_right_button_down = false;

            {
                //
                // command handling
                //

                std::string command;
                std::lock_guard<std::mutex> lock(_gesture_list_lock);
                if (false == _gesture_list.empty()) {
                    for (const auto& gesture : _gesture_list) {
                        
                        if (mouse_gesture::left == gesture) {
                            command.push_back('L');
                        }
                        else if (mouse_gesture::up == gesture) {
                            command.push_back('U');
                        }
                        else if (mouse_gesture::down == gesture) {
                            command.push_back('D');
                        }
                        else if (mouse_gesture::right == gesture) {
                            command.push_back('R');
                        }
                    }

                    OutputDebugStringA(command.c_str());

                    if (execute_command(command)) {
                        
                        //
                        // execute command.
                        //

                        return 1;
                    }
                }
            }
        }
        else {
            break;
        }

    } while (false);

    return ::CallNextHookEx(_hook, code, wparam, lparam);
}
keyboard_input은 특정 기능을 위해서 사용하시면 될 것 같습니다. 예를 들어 위에서 처럼 command 처리 시 키보드 HOME 버튼을 시스템에 전달하거나, command 처리시 키보드 END 버튼을 전달하는 등 처리가 가능하며, 그 외에 ShellExecute, 모든 동작이 가능하기 때문에 command에 따라 원하는 기능을 구현하시면 됩니다.

 

여기서 핵심은 execute_command 호출 후 성공 시 return 1 처리를 하는 것에 있습니다.

이는 마우스 오른쪽 마우스 클릭이 콘텍스트 메뉴가 생성되게 되는데, 마우스 커맨드가 일어나는 시점에는 컨텍스트 메뉴가 뜨지 않게 하기 위함이기때문에 만약 마우스 로그 등의 기록을 위한경우에는 return 1 코드를 제거하여 컨텍스트 메뉴가 정상적으로 동작하도록 해야 합니다.

 

현재 만들어서 사용중인 프로그램은 개인적으로 만들어서 사용하다보니 UI 관련 부분이 완료가 안되어있어서 배포는 추후 할 예정입니다. 상관 없이 원하시면 비밀댓글로 요청해주세요 :) 해당 포스팅이 많은 분들에게 도움이 되었으면 좋겠습니다.

 

Sample Download

analog_note_mouse_hook.zip
0.07MB

반응형