버튼 오른쪽 아래로 정렬하는 예제입니다. 아래 Launcher Template 예제의 MainWindow.__init__() 부분에 교체하여 테스트할 수 있습니다.

vbox = wx.BoxSizer(wx.VERTICAL)

button = wx.Button(self, id = 108, label = "TITLE_8", size = (120, 50))
hbox = wx.BoxSizer(wx.HORIZONTAL)
hbox.AddStretchSpacer()
hbox.Add(button)
hbox.AddSpacer(10) # optional

self.Bind(wx.EVT_BUTTON, self.OnButton, button)

vbox.AddStretchSpacer()
vbox.Add(hbox, 0, wx.EXPAND | wx.ALL)
vbox.AddSpacer(10) # optional

self.SetSizer(vbox, True)
self.Layout()

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

wxPython에서 별도의 어플리케이션 아이콘을 지정하지 않으면 기본 아이콘으로 표시된다. 이를 원하는 아이콘으로 지정하는 방법에 대해 정리해보았다.

  1. 프레임 창 좌측 상단에 표시되는 아이콘은 다음의 방법으로 바꿀 수 있다. PNG 파일 외에 다른 타입의 이미지를 사용하려면 wx.BITMAP_TYPE_PNG를 해당하는 상수로 바꿔주면 된다. (예를 들어 아이콘 파일(.ICO)의 경우는 wx.BITMAP_TYPE_ICO)
    class MainWindow(wx.Frame):
        def __init__(self, parent, id, title):
    
            ...
    
            ICON_PATH = 'icon_image.png'
            self.SetIcon(wx.Icon(ICON_PATH, wx.BITMAP_TYPE_PNG))
    
    
  2. 태스크바의 아이콘까지 바꾸려면 아래의 작업을 추가로 해주어야 한다.
    import ctypes   # for taskbar icon
    
    ...
    
    class MainWindow(wx.Frame):
        def __init__(self, parent, id, title):
    
            ...
    
            my_app_id = r'mycompany.myproduct.subproduct.version'   # 임의의 스트링
            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_app_id)
    
    


- OS : Windows 10
- Python 버전 : 3.6.5
- wxPython 버전 : 4.1.1
,

>> [Python] 키보드 후킹 - 2. 간편화 코드


콜백 함수에 getFPTR을 호출하여 함수 포인터 얻는 것을 installHookProc 함수 안에서 하도록 처음 시도하였을 때 에러가 발생하였다.
        pointer = KeyRogue.getFPTR(KeyRogue.hookProcInternal)
         
        KeyRogue.hooked = KeyRogue.user32.SetWindowsHookExA(
            KeyRogue.WH_KEYBOARD_LL,
            pointer,
            KeyRogue.kernel32.GetModuleHandleW(None),
            0
        )

위 코드와 같이 getFPTR을 호출한 결과를 지역 변수로 받아서 SetWindowsHookExA 함수의 파라미터로 넘겼는데, static 하지않은 변수를 파라미터로 넘긴 것이 문제가 된 것이었다. 최종 코드와 같이 클래스 변수를 사용하여 파라미터를 넘겼을 때는 문제가 발생하지 않았다.

,

윈도우 환경에서 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)


,

윈도우 환경에서 Python을 사용하여 키보드 후킹을 하는 방법의 이해를 돕기 위한 예제 코드이다.



소스

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

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

user32 = windll.user32
kernel32 = windll.kernel32

WH_KEYBOARD_LL = 13
WM_KEYDOWN = 0x0100
CTRL_CODE = 162

class KeyRogue:
    def __init__(self):
        self.lUser32 = user32
        self.hooked = None

    def installHookProc(self, pointer):
        
        self.hooked = self.lUser32.SetWindowsHookExA(
            WH_KEYBOARD_LL,
            pointer,
            kernel32.GetModuleHandleW(None),
            0
        )
        if not self.hooked:
            return False
        return True

    def uninstallHookProc(self):
        if self.hooked is None:
            return
        self.lUser32.UnhookWindowsHookEx(self.hooked)
        self.hooked = None

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

    def startKeyLog(self):
        msg = MSG()
        user32.GetMessageA(byref(msg), 0, 0, 0)

    def getKeyCode(self, lParam):
        high, low = self.bytes(lParam[0])
        return low
   
def hookProc(nCode, wParam, lParam):
    if wParam is not WM_KEYDOWN:
        return user32.CallNextHookEx(keyRogue.hooked, nCode, wParam, lParam)
    keyCode = keyRogue.getKeyCode(lParam)
    hookedKey = chr(keyCode)
    print(hookedKey)
    if (CTRL_CODE == keyCode):
        print("ctrl pressed, call uninstallHook()")
        keyRogue.uninstallHookProc()
        sys.exit(-1)
    return user32.CallNextHookEx(keyRogue.hooked, nCode, wParam, lParam)

keyRogue = KeyRogue()
pointer = keyRogue.getFPTR(hookProc)
if keyRogue.installHookProc(pointer):
    print("install success")
    keyRogue.startKeyLog()
else:
    print("install failed")


문제 해결

  • Python 3.7.5 64비트 버전에서 SetWindowsHookExA 호출 시에 0값이 리턴되면서 Hook 설치가 실패되는 일이 있었는데, 3.7.5 32비트 버전을 설치하니까 해결이 되었다. 그런데 3.6.5 64비트에서는 또 문제가 없고.. 잘 안될 경우는, 버전 혹은 비트 수를 변경해서 시도해볼 필요가 있다.


참고사항

<테스트 환경> - 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
,

표준 라이브러리인 configparser를 사용하면 .INI 파일을 읽고 쓸 수 있다.



.INI 파일 쓰기

import configparser

config = configparser.ConfigParser()
config['AAA'] = {}                # 섹션을 생성한다
config['AAA']['BBB'] = 'CCC'      # 섹션 아래 실제 값을 생성한다
config['DEFAULT']['DDD'] = 'EEE'      # DEFAULT 섹션은 기본적으로 생성되어 있어 생성없이 쓸 수 있다

with open('config.ini', 'w') as configfile:
    config.write(configfile)

  • 1 Depth에 반드시 섹션을 생성한 후에 그 아래 값을 생성할 수 있다. (1 Depth에는 값을 생성할 수 없다)
  • Depth는 최대 2 Depth까지 가능하다. (2 Depth에는 섹션을 생성할 수 없다)


.INI 파일 읽기 (기본)

import configparser

config = configparser.ConfigParser()
config.read('config.ini')

print("config['AAA']['BBB'] : " + config['AAA']['BBB'])


.INI 파일 읽기 (고급)

import configparser

config = configparser.ConfigParser()
config.read('config.ini')

if not 'AAA' in config:           # 섹션이 존재하는지 체크
    print("config['AAA'] not exist")
   
if 'BBB' in config['AAA']:        # 섹션 아래 값이 존재하는지 체크
    print("config['AAA']['BBB'] : " + config['AAA']['BBB'])
else:
    print("config['AAA']['BBB'] not exist")
    
if 'DDD' in config['AAA']:        # 섹션 아래 값이 존재하는지 체크
    print("config['AAA']['DDD'] : " + config['AAA']['DDD'])
else:
    print("config['AAA']['DDD'] not exist")

  • Config 파일이 존재하지 않을 경우는, 에러가 나지 않고 단지 빈 맵으로 반환된다.(DEFAULT 섹션은 존재한다)


참고사항

<테스트 환경>
 - OS : Windows 10
 - Python 버전 : 3.7.5
,

인스턴스 메소드

  • 인스턴스에 대한 참조를 첫번째 파라미터로 받는다.
  • 정의 방법
  • class ClassName1:
      def method1(self, a, b, c):
        ...
    


정적(static) 메소드

  • 필수 파라미터가 없다.
  • 정의 방법
  • class ClassName2:
      @staticmethod
      def method2(a, b, c):
        ...
    


클래스 메소드

  • 클래스 정보 객체에 대한 참조를 첫번째 파라미터로 받는다.
  • 정의 방법
  • class ClassName3:
      @classmethod
      def method3(cls, a, b, c):
        ...
    
<테스트 환경> 
- OS : Windows 10 
- Python 버전 : 3.7.5
,