윈도우 환경에서 Python을 사용하여 키보드 후킹을 하는 방법 관련하여 좀더 쓰기 편하게 코드 정리를 하였다. 특수키를 확인하는 방법도 추가되었다.



소스


KeyRogue.py
# -*- coding: utf-8 -*-
#!/usr/bin/python

import sys
from ctypes import *
from ctypes.wintypes import MSG
from ctypes.wintypes import DWORD

class KeyRogue:
    user32 = windll.user32
    kernel32 = windll.kernel32

    WH_KEYBOARD_LL = 13
    WM_KEYDOWN = 0x0100
    WM_SYSKEYDOWN = 0x0104
    
    VK_CONTROL = 0x11
    VK_MENU = 0x12
    VK_SHIFT = 0x10
    
    hooked = None
    hookProc = None
    pointer = None
    
    @staticmethod
    def installHookProc(hookProc):
        if KeyRogue.hooked is not None:
            print("already installed. uninstalling..")
            KeyRogue.uninstallHookProc()
        KeyRogue.hookProc = hookProc
        KeyRogue.pointer = KeyRogue.getFPTR(KeyRogue.hookProcInternal)
        
        KeyRogue.hooked = KeyRogue.user32.SetWindowsHookExA(
            KeyRogue.WH_KEYBOARD_LL,
            KeyRogue.pointer,
            KeyRogue.kernel32.GetModuleHandleW(None),
            0
        )
        if not KeyRogue.hooked:
            return False
        return True

    @staticmethod
    def uninstallHookProc():
        if KeyRogue.hooked is None:
            return
        KeyRogue.user32.UnhookWindowsHookEx(KeyRogue.hooked)
        KeyRogue.hooked = None

    @staticmethod
    def bytes(integer):
        return divmod(integer, 0x10000)
            
    @staticmethod
    def getFPTR(fn):
        CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p))
        return CMPFUNC(fn)

    @staticmethod
    def startKeyLog():
        msg = MSG()
        KeyRogue.user32.GetMessageA(byref(msg), 0, 0, 0)

    @staticmethod
    def getKeyCode(lParam):
        high, low = KeyRogue.bytes(lParam[0])
        return low

    @staticmethod
    def getKeyInfo(lParam):
        keyCode = KeyRogue.getKeyCode(lParam)
        return (keyCode, chr(keyCode))

    @staticmethod
    def checkModifier(lParam, vKey):
        if KeyRogue.user32.GetAsyncKeyState(vKey) & 0x8000:
            return True
        return False

    @staticmethod
    def hookProcInternal(nCode, wParam, lParam):
        if wParam != KeyRogue.WM_KEYDOWN and wParam != KeyRogue.WM_SYSKEYDOWN:
            return KeyRogue.user32.CallNextHookEx(KeyRogue.hooked, nCode, wParam, lParam)
        if KeyRogue.hookProc:
            KeyRogue.hookProc(nCode, wParam, lParam)
        return KeyRogue.user32.CallNextHookEx(KeyRogue.hooked, nCode, wParam, lParam)

    @staticmethod
    def start(hookProc):
        if KeyRogue.installHookProc(hookProc):
            KeyRogue.startKeyLog()
        else:
            print("install failed")

    @staticmethod
    def stop():
        KeyRogue.uninstallHookProc()

client.py
from KeyRogue import *

def hookProc(nCode, wParam, lParam):
    key = KeyRogue.getKeyInfo(lParam)
    print("key code = %d" % key[0])
    print("key char = %c" % key[1])
    if KeyRogue.checkModifier(lParam, KeyRogue.VK_CONTROL):
        print("CTRL pressed")
        if (key[1] == 'Q'):
            print("exit key pressed, call stop()")
            KeyRogue.stop()
            sys.exit(-1)
    if KeyRogue.checkModifier(lParam, KeyRogue.VK_MENU):
        print("ALT pressed")
    if KeyRogue.checkModifier(lParam, KeyRogue.VK_SHIFT):
        print("SHIFT pressed")

KeyRogue.start(hookProc)

  • Hook 설치 성공시 .start() 함수는 리턴되지 않는다.(심지어 stop() 함수를 사용하여 언인스톨되었을 경우에도) 따라서 GUI가 있는 프로그램에 키보드 후킹 기능을 추가하려면 별도의 쓰레드에서 실행되도록 처리가 필요하다.


참고사항

<테스트 환경>
- OS : Windows 10
- Python 버전 : 3.6.5(32/64bit), 3.7.5(32bit)


,

.INI 파일에 설정을 읽고 쓰는 것을 간편하게 하기 위한 boilerplate(부뚜막?) 코드



개요


다음 의식의 흐름에 의해 처리된다.

  1. 소스 상의 디폴트 설정값을 적용한다.
  2. .INI 파일이 존재할 경우 .INI 파일 상의 설정값을 적용한다.
  3. 최종 설정값을 .INI 파일에 저장한다.


소스


easyconfig.py
# -*- coding: utf-8 -*-
#!/usr/bin/python

import configparser

class EasyConfig:

    SECTION_DEFAULT = 'DEFAULT'
    configFileName = None

    # 설정 파일명을 세팅함
    def setFileName(self, fileName):
        self.configFileName = fileName

    # 파일로부터 설정을 세팅함
    def loadConfig(self, configParam):
        if not self.configFileName:
            return
        
        configFile = configparser.ConfigParser()
        configFile.read(self.configFileName)
        
        for key in configParam.keys():
            if key in configFile[self.SECTION_DEFAULT]:
                if type(configParam[key]) == int:
                    try:
                        val = int(configFile[self.SECTION_DEFAULT][key])
                        configParam[key] = val
                    except:
                        pass
                elif type(configParam[key]) == float:
                    try:
                        val = float(configFile[self.SECTION_DEFAULT][key])
                        configParam[key] = val
                    except:
                        pass
                elif type(configParam[key]) == bool:
                    if configFile[self.SECTION_DEFAULT][key] == 'True':
                        configParam[key] = True
                    else:
                        configParam[key] = False
                else:
                    configParam[key] = configFile[self.SECTION_DEFAULT][key]

    # 현재 설정을 파일로 저장함
    def saveConfig(self, configParam):
        if not self.configFileName:
            return
        
        configFile = configparser.ConfigParser()
        
        for key in configParam:
            val = None
            if type(configParam[key]) == int:
                val = "%d" % configParam[key]
            elif type(configParam[key]) == float:
                val = "%f" % configParam[key]
            elif type(configParam[key]) == bool:
                if configParam[key]:
                    val = 'True'
                else:
                    val = 'False'
            elif type(configParam[key]) == str:
                val = configParam[key]

            if val:
                configFile[self.SECTION_DEFAULT][key] = val
        
        with open(self.configFileName, 'w') as writeFile:
            configFile.write(writeFile)

    # 현재 설정 내용을 출력함
    def dumpConfig(self, configParam):
        for key in configParam:
            if type(configParam[key]) == int:
                print("%s = %d[int]" % (key, configParam[key]))
            elif type(configParam[key]) == float:
                print("%s = %f[float]" % (key, configParam[key]))
            elif type(configParam[key]) == bool:
                if configParam[key]:
                    print("%s = True[bool]" % key)
                else:
                    print("%s = False[bool]" % key)
            elif type(configParam[key]) == str:
                print("%s = %s[str]" % (key, configParam[key]))

    def process(self, configParam):
        self.loadConfig(configParam)
        self.saveConfig(configParam)

  • DEFAULT 섹션만을 사용한다. 그 외의 섹션을 사용하려면 섹션 존재 여부를 확인하는 추가적인 처리가 필요하다.


사용 방법


client.py
from easyconfig import *

# 설정 디폴트 값을 맵으로 정의한다
configDef = {}
configDef["A"] = "B"
configDef["C"] = "D"
configDef["E"] = 12
configDef["F"] = 3.14
configDef["G"] = True

easyConfig = EasyConfig()
easyConfig.setFileName('ec_config.ini') # 설정 파일명을 정의한다
easyConfig.process(configDef) # 설정 파일을 처리한다

  • int, float, bool, 문자열 타입만 지원한다. 설정 맵 상에 그 외의 타입이 존재할 경우 저장은 되지 않는다.


참고사항

<테스트 환경>
- OS : Windows 10
- Python 버전 : 3.7.5
,
  • my-rewind : 버퍼 내에 맨 앞으로 이동하는 함수
  • my-replace : 현재 위치로부터 뒤로 검색하면서 특정 문자열을 다른 문자열로 모두 바꿔주는 함수 (bound: 특정 위치까지 검색. nil이면 버퍼끝까지)
  • my-replace-1t : 현재 위치로부터 뒤로 검색하면서 특정 문자열을 다른 문자열로 1회 바꿔주는 함수. 대체가 일어났을 경우 t를, 아니면 nil을 리턴 (bound: 특정 위치까지 검색. nil이면 버퍼끝까지)


함수 정의

(defun my-rewind ()
  (goto-char (point-min))
  )

(defun my-replace (str-from str-to bound)
  (progn
    (while (search-forward str-from bound t)
      (progn
        (delete-backward-char (length str-from))
        (insert str-to)
		)
	  )
	)
  )

(defun my-replace-1t (str-from str-to bound)
  (progn
    (if (search-forward str-from bound t)
		(progn
		  (delete-backward-char (length str-from))
		  (insert str-to)
		  t
		  )
	  nil
	  )
	)
  )


사용 예

(defun my-replace-example ()
  (interactive)

  (my-rewind)
  (my-replace "abc" "def" nil)  ;버퍼내에 모든 'abc'를 'def'로 바꿈
  )

,
프롬프트로부터 값을 입력받아 처리하는 함수 정의입니다.
(defun <함수명> ()
  (interactive (let ((<변수명>  (read-string "<프롬프트 문자열>")))
                     <처리할 구문>
                     ))
)


<테스트 환경> 
- OS : Windows 7 
- Emacs 버전 : Emacs 24.3 윈도우
,

태그명을 입력받아 한쌍의 열기/닫기 태그를 한번에 입력해주는 함수 정의입니다.

(defun enter-tag ()
  (interactive (let ((tag-name  (read-string "Tag name?")))
                 (insert (format "<%s>\n</%s>" tag-name tag-name))
                 ))
  )


<테스트 환경>
- OS : Windows 7
- Emacs 버전 : Emacs 24.3 윈도우
,
외부 프로그램을 실행하고 그 표준 출력/에러를 텍스트 컨트롤에 표시하는 어플의 Template입니다.


template2.py
# -*- coding: utf-8 -*-
#!/usr/bin/python
 
import wx
import subprocess
import threading
import sys
 
COMMAND_PATH = "test.py" # 실행할 외부 프로그램 경로

def stdout_thread(pipe, callback):
    for line in iter(pipe.stdout.readline, b''):
        callback(line.rstrip())

def exec_command(cmd, callback, cwd=None):
    p = subprocess.Popen(cmd, 
                         stdout=subprocess.PIPE, 
                         stderr=subprocess.STDOUT, cwd=cwd)
    out_thread = threading.Thread(name='stdout_thread', target=stdout_thread, args=(p, callback))

    out_thread.start()
 
class MainWindow(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size = (480, 320),
            style = wx.DEFAULT_FRAME_STYLE|wx.NO_FULL_REPAINT_ON_RESIZE)
             
        vbox = wx.BoxSizer(wx.VERTICAL)

        self.tc = wx.TextCtrl(self, 1, size = (-1, 200), style = wx.TE_MULTILINE)
        vbox.Add(self.tc, 0, wx.EXPAND | wx.ALL)

        self.SetSizer(vbox, True)
        self.Layout()
         
def callback_stdout(msg):
    print("}}} " + msg)
    frame.tc.AppendText(msg + "\n")
             
app = wx.App(False)
frame = MainWindow(None, -1, u"Title")
frame.Show(1)

exec_command([sys.executable, COMMAND_PATH], callback_stdout)

app.MainLoop()


test.py
# -*- coding: utf-8 -*-
#!/usr/bin/python

import sys

print "blah;blah;"
sys.stdout.flush()


[주의사항] 실행하려는 외부 프로그램이 python 코드일 때, 외부 프로그램에서 메세지 출력 후 sys.stdout.flush()를 해줘야 메세지가 그때 그때 출력된다. 그렇지 않으면 메세지가 마지막에 한꺼번에 출력된다.


template2.py 실행 화면


 


<테스트 환경>
OS : Windows 7
Python 버전 : 2.7
wxPython 버전 : 2.8.12.1


,

버튼을 누르면 특정 프로그램이 실행되는 Launcher 어플의 Template입니다.


# -*- coding: utf-8 -*-
#!/usr/bin/python

import wx
import subprocess

BUTTON_ID_1 = 101
BUTTON_ID_2 = 102
BUTTON_ID_3 = 103
BUTTON_ID_4 = 104

COMMAND_PATH_1 = "" # 첫번째 버튼으로 실행할 실행파일 경로 예) "C:\\Windows\\explorer.exe"
COMMAND_PATH_2 = "" # 두번째 버튼으로 실행할 실행파일 경로
COMMAND_PATH_3 = "" # 세번째 버튼으로 실행할 실행파일 경로
COMMAND_PATH_4 = "" # 네번째 버튼으로 실행할 실행파일 경로

COMMAND_PARAM_1 = "" # 첫번째 버튼으로 실행할 실행파일 파라미터
COMMAND_PARAM_2 = "" # 두번째 버튼으로 실행할 실행파일 파라미터
COMMAND_PARAM_3 = "" # 세번째 버튼으로 실행할 실행파일 파라미터
COMMAND_PARAM_4 = "" # 네번째 버튼으로 실행할 실행파일 파라미터

def ShellExecute(path, param="", cwd=None, nonExecutable = False):
    # path  : 실행 파일 경로
    # param : 파라미터
    # cwd   : 현재 디렉토리
    # nonExecutable : True일 경우, 실행 파일이 아니어도 연결 프로그램으로 실행 가능
    try:
        if param == "":
            subprocess.Popen([path], cwd = cwd, shell=nonExecutable)
        else:
            subprocess.Popen([path, param], cwd = cwd, shell=nonExecutable)
    except OSError:
        wx.MessageBox(u"Launch Failed!", u"Info")

class MainWindow(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size = (320, 240),
            style = wx.DEFAULT_FRAME_STYLE|wx.NO_FULL_REPAINT_ON_RESIZE)
            
        vbox = wx.BoxSizer(wx.VERTICAL)

        button = wx.Button(self, id = BUTTON_ID_1, label = u"Button 1", size = (-1, 50))
        vbox.Add(button, 0, wx.EXPAND | wx.ALL)
        self.Bind(wx.EVT_BUTTON, self.OnButton, button)
        
        button = wx.Button(self, id = BUTTON_ID_2, label = u"Button 2", size = (-1, 50))
        vbox.Add(button, 0, wx.EXPAND | wx.ALL)
        self.Bind(wx.EVT_BUTTON, self.OnButton, button)

        button = wx.Button(self, id = BUTTON_ID_3, label = u"Button 3", size = (-1, 50))
        vbox.Add(button, 0, wx.EXPAND | wx.ALL)
        self.Bind(wx.EVT_BUTTON, self.OnButton, button)

        button = wx.Button(self, id = BUTTON_ID_4, label = u"Button 4", size = (-1, 50))
        vbox.Add(button, 0, wx.EXPAND | wx.ALL)
        self.Bind(wx.EVT_BUTTON, self.OnButton, button)

        self.SetSizer(vbox, True)
        self.Layout()
        
    def OnButton(self, event):
        buttonId = event.GetId()
        
        if buttonId == BUTTON_ID_1:
            ShellExecute(COMMAND_PATH_1, COMMAND_PARAM_1)
        if buttonId == BUTTON_ID_2:
            ShellExecute(COMMAND_PATH_2, COMMAND_PARAM_2)
        if buttonId == BUTTON_ID_3:
            ShellExecute(COMMAND_PATH_3, COMMAND_PARAM_3)
        if buttonId == BUTTON_ID_4:
            ShellExecute(COMMAND_PATH_4, COMMAND_PARAM_4)

            
app = wx.App(False)
frame = MainWindow(None, -1, u"Title")
frame.Show(1)
app.MainLoop()




<테스트 환경>
OS : Windows 7
Python 버전 : 2.7
wxPython 버전 : 2.8.12.1


,