• 카테고리

    질문 & 답변
  • 세부 분야

    프로그래밍 언어

  • 해결 여부

    미해결

나만의 단축키만들기 코드 실행시 오류가 있는거같습니다

20.01.13 20:25 작성 조회수 348

1

from pynput.keyboard import Key, Listener, KeyCode
import win32api

MY_HOTKEY = [
    {"function1": {Key.ctrl_l, Key.alt_l, KeyCode(char="c")}},
    {"function2": {Key.shift, Key.ctrl_l, KeyCode(char="n")}},
    {"function3": {Key.alt_l, Key.ctrl_l, KeyCode(char="g")}}
]

def function1():
    print("function1 called")
    win32api.WinExec("calc.exe")

def function2():
    print("function2 called")
    win32api.WinExec("notepad.exe")

def function3():
    print("function3 called")
    win32api.WinExec("C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe")

current_keys=set()

def key_pressed(key):
    for data in MY_HOTKEY: #핫키안의 딕셔너리를 data로 접근후 key와 value로 구분
        FUNCTION = list(data.keys())[0]
        KEYS = list(data.values())[0]
        
        if key in KEYS:
            current_keys.add(key)#누른 키를 저장
            print(current_keys)
            if all(k in current_keys for k in KEYS):
                function= eval(FUNCTION)
                function()


def key_released(key):
    if key in current_keys:
        current_keys.remove(key)

    if key==Key.esc:
        return False

with Listener(on_press=key_pressed, on_release = key_released) as listener:
   listener.join()
강의에서 보여주신 코드 그대로 적었습니다만
current_keys에 담기는 키값에 문제가 발생하는것 같습니다
currnet_keys가 빈상태로 시작해야하는데
핫키로 몇번 실행하다보면 실행은 잘되는데 언제부턴가
예를들면 current_key값에 c가 담겨서
alt_l + ctrl_l 만 눌러도 calculator가 실행이 된다든지 합니다
current_keys를 출력해봐도 'g'를 눌렀는데 {'g','c'}가 나옵니다
저는 ctrl_l과 문자를 같이누르면 문자가 다른것으로 변하는것 때문에
문자먼저 누르고 ctrl alt를 누르는데,
이것 때문일까요?

답변 3

·

답변을 작성해보세요.

1

해당 부분은 저도 직접 분석을 해본게 아니라서 정확하게 말씀드리기가 어렵습니다만..

일단 문제가 있는 다른 분들의 이야기를 종합해보면 공통점이 키보드의 키 입력이 중첩되었을때 키코드가 아스키값으로 인식되는 문제가 있습니다. 이게 하드웨어상의 문제인지 아니면 라이브러리의 문제인지 저도 직접 본게 아니라서 판단하기가 어렵습니다.

어쨌든 ctrl_l 과 문자를 같이 누르면 다른것으로 변한다고 하신 사항이 이와 비슷한 문제가 있지 않을까 의심이 됩니다만 정확하게 어떤 다른것으로 변하는지 말씀을 주시지 않았기에 추측만 해봅니다. 어떤걸로 변하는지 혹시 코드값이 바뀌는지는 아래의 링크를 참고해서 보시면 될듯 합니다. 아래 링크 내용에서 숫자가 키보드에 해당하는 아스키코드값입니다. 예를 들어 c 라는 키의 코드값은 67 입니다.

[아스키코드값 링크]

그리고 컴퓨터의 환경은 저마다 너무 달라서 제가 정확하게 진단할수는 없는 부분이 많습니다. 태영님께서는 스터디를 굉장히 열심히 하시는거 같아서 좀 더 자세한 문제 해결 방법을 말씀드리자면...

프로그램을 작성하다보면 항상 오류와 문제를 만나게 됩니다. 여기서 가장 중요한건 과연 정확한 문제점이 어떤건지부터 확인해야 합니다. 일단 위의 상황을 보면 말씀하신 키가 눌리는데서 생기는 문제인지 혹은 눌렸던 키가 지워지지 않는 문제인지 부터 정확하게 분석해보셔야 할듯 합니다. press 이벤트와 release 이벤트에 모두 print()를 하셔서 어느시점에 어떤 문제가 생기는지 부터 확인해보시길 추천합니다. 몇번 실행하다 보면 current_key에 c가 담긴 이유가 지워지지 않은건지 혹은 c를 눌렀는데 어떤 타이밍에 그걸 다른 코드로 인식해서 지울 수 없던건지 명확해야 할듯 합니다. 다른코드라면 그 코드는 무엇인지도 명확해야 합니다. 만약 위에서 처럼 c를 67로 인식한거라면 애초에 KeyCode(char="c") 를 KeyCode(67) 로 등록하셔도 됩니다. 

그리고 위에서 말씀하셨던 press 이벤트에서 키 코드가 다르게 작성된다는 점도 실제 라이브러리의 소스코드까지 분석을 해보시길 바랍니다.  아래 내용은 다른 분께 드렸던 답변인데 같은 내용이라 붙여넣기 했습니다....

사실 pynput 은 윈도우의 경우 WH_KEYBOAD_LL 라는 메세지를 후킹하는 DLL을 파이썬에서 사용하게 래핑한 라이브러리 입니다. (윈도우는 모든 동작이 메세지 형태로 일어나며 그 메세지를 가로채는걸 후킹 한다고 합니다) 그 말인 즉슨 pynput 은 파이썬과 C++로 작성된 DLL 파일과의 다리 역할만 하고 실제 키를 감지, 키를 발생시키는 애는 윈도우 자체가 하고 있는걸로 보입니다.

VS CODE 상에서

listener = keyboard.Listener(
    on_press=on_press,
    on_release=on_release)
listener.start()

키보드를 리스닝하는 keyboard.Listener 코드 에서 Listener를 컨트롤키 + 클릭 하면 pynput 소스 코드로 진입할 수 있습니다. 그러면 거기서 



if sys.platform == 'darwin':
    if not KeyCode and not Key and not Controller and not Listener:
        from ._darwin import KeyCode, Key, Controller, Listener

elif sys.platform == 'win32':
    if not KeyCode and not Key and not Controller and not Listener:
        from ._win32 import KeyCode, Key, Controller, Listener

else:
    if not KeyCode and not Key and not Controller and not Listener:
        try:
            from ._xorg import KeyCode, Key, Controller, Listener
        except ImportError:
            # For now, since we only support Xlib anyway, we re-raise these
            # errors to allow users to determine the cause of failures to import
            raise

위와 같은 부분이 있는데 윈도우인 경우 sys.platform == 'win32' 밑의 코드에서

from ._win32 import KeyCode, Key, Controller, Listener

여기서 Listener 를 컨트롤 + 클릭 하시면 Listener 클래스의 소스 코드를 보실 수 있습니다. 여기서.. 밑으로 스크롤을 조금 내려보시면

@AbstractListener._emitter
    def _process(self, wparam, lparam):
        msg = wparam
        vk = lparam

        # If the key has the UTF-16 flag, we treat it as a unicode character,
        # otherwise convert the event to a KeyCode; this may fail, and in that
        # case we pass None
        is_utf16 = msg & self._UTF16_FLAG
        if is_utf16:
            msg = msg ^ self._UTF16_FLAG
            scan = vk
            key = KeyCode.from_char(six.unichr(scan))
        else:
            try:
                key = self._event_to_key(msg, vk)
            except OSError:
                key = None

        if msg in self._PRESS_MESSAGES:
            self.on_press(key)

        elif msg in self._RELEASE_MESSAGES:
            self.on_release(key)

위의 코드가 on_press 되었을때 호출되는 부분이고 여기 하단의 self.on_press가 호출되는데 이 함수는 _base.py 파일에

def __init__(self, on_press=None, on_release=None, suppress=False,
                 **kwargs):
        prefix = self.__class__.__module__.rsplit('.', 1)[-1][1:+ '_'
        self._options = {
            key[len(prefix):]: value
            for key, value in kwargs.items()
            if key.startswith(prefix)}
        super(Listener, self).__init__(
            on_press=on_press, on_release=on_release, suppress=suppress)

위처럼 초기화 되면서 on_press 함수가  내가 작성한 on_press 함수로 연결되는 부분인걸 확인 하실 수 있습니다. (복잡합니다...)

어쨌든 다시 위의 코드를 보면

def _process(self, wparam, lparam):

이 함수의 인자로 넘어오는 wparam 은 윈도우 메세지 이름이고 lparam 값이 키보드의 키 코드 값입니다. 이런식으로  위에서 말씀드린 컨트롤 + 클릭으로 pynput 소스코드를 분석하셔서 필요한곳에 print 문 같은걸 찍어서 확인해보시는면 찾아볼 수 있지 않을까 생각됩니다. 물론 라이브러리의 소스 코드를 수정하는건 신경써서 하셔야 하고 프린트문 찍었다가 지우시고 만약 라이브러리가 꼬이면 삭제하시고 다시 설치하시면 됩니다. 도움이 되셨으면 좋겠습니다.

0

해결하셨다니 축하드립니다. ^^

0

윤태영님의 프로필

윤태영

질문자

2020.01.14

친절한 답변 감사합니다

release할때 press할때에 저장된 키값을 모두 삭제하는 것으로 해결되었습니다

def key_released(key):
    current_keys.clear()
    print(current_keys)

    if key==Key.esc:
        return False