PythonによるWindowsメッセージフック・前編の続きです。前編ではクリックに応じてコンソールウィンドウに、クリックが発生した時間と画面上の座標をprint
するPythonスクリプトを作成しましたが、次のような問題のあるものでした。
- Pythonスクリプトの範囲で正しく終了できない
- フックで取得した情報をPythonスクリプト側で利用できない
後編のこの記事では、まず以上の問題点を解決した実用にかなう、マウスアクションをフックするPythonスクリプトの例を示したいと思います。
改修案
新しいフックスクリプトを次のように変更します。
- スクリプトの終了を防ぐために、
GetMessageA
の代わりにPySimpleGUIのメッセージループを使う
PySimpleGUIのread()
関数もGetMessageA
のようにメッセージを待機する関数なので、スクリプトの終了を防ぐことができます - フックプロシージャをKeyLoggerクラスに統合する
クラスに属する関数として定義することで、フックプロシージャがself
を介して属性にアクセスすることができます。Python側からはKeyLoggerクラスのインスタンスの属性を参照すればフックプロシージャからの情報が取得できます
マウスフックスクリプト
新しいスクリプトは以下のようになります。
import win32con
import ctypes
import ctypes.wintypes as win
import PySimpleGUI as sg
layout = [[sg.Text('text', key='TEXT')],
[sg.Button('クリック数を表示', key='BUTTON')]]
window = sg.Window('win32api_test', layout)
class MsStructuer(ctypes.Structure):
_fields_ = [('point', win.POINT),
('mouseDate', win.DWORD),
('flags', win.DWORD),
('time', win.DWORD),
('dwExtraInfo', ctypes.c_ulong)]
class KeyLogger:
def __init__(self):
self.User32 = ctypes.WinDLL('user32', use_last_error=True)
self.hooked = None
self.cnt = 0
def installHookProc(self,pointer):
self.hooked = self.User32.SetWindowsHookExA(win32con.WH_MOUSE_LL, pointer, None, 0)
if not self.hooked:
return False
return True
def uninstalHookProc(self):
if self.hooked is None:
return
self.User32.UnhookWindowsHookEx(self.hooked)
self.hooked = None
def hookProc(self, nCode, wParam, lParam):
if nCode == win32con.HC_ACTION and wParam == win32con.WM_LBUTTONDOWN:
ms = MsStructuer.from_address(lParam)
self.cnt += 1
print(f'{ms.time}:{ms.point.x}, {ms.point.y}')
return self.User32.CallNextHookEx(0, nCode, win.WPARAM(wParam), win.LPARAM(lParam))
kl = KeyLogger()
HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_int, win.WPARAM, win.LPARAM)
callback = HOOKPROC(kl.hookProc)
bool = kl.installHookProc(callback)
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Exit'):
kl.uninstalHookProc()
break
if event == 'BUTTON':
window['TEXT'].update(f'クリック数:{kl.cnt}回')
window.close()
KeyLoggerクラスのcnt
を介して、Pythonスクリプトがクリックの回数を取得できるようになりました。フックのアンインストールは、ウィンドウクローズのプロセスに挿入しています。
この例では、ウィンドウの生成前にフックのインストールを行っていますが、PySimpleGUIのボタンメッセージをトリガーにして、フックのインストール、アンインストールを行うこともできます。
キーボードフック
最後に低レベルのキーボードフックについて簡単に紹介します。残念ながらアルファベットや数字の入力はキャプチャーどまりで、漢字やひらがなの文字列としてキーボードの入力をキャプチャーできるわけではありません。
キーボードフックの場合SetWindowsHookExA
のidHook
値に、整数値で「13」を指定します。これはWH_KEYBOARD_LLという定数として定義されており、win32conにも同様のものが定義されています。
SetWindowsHookExA(win32con.WH_KEYBOARD_LL, callback, None, 0)
コールバック関数は次のように定義されています。
LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
マウスのコールバック関数との違いは名前だけのように見えますが、wParamとlParamの内容が異なります。
wParamはWPARAM型の定数、WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN、WM_SYSKEYUPのいずれかのメッセージで、キーボードに対するアクションを知らせてくれます。これらの定数もやはりwin32conで定義されています。
lParamはKBDLLHOOKSTRUCT構造体へのポインタです。マウスの場合と同じように、構造体の互換型を用意して、from_address
関数を使って情報を取得します。
class KBDLLHOOKSTRUCT(ctypes.Structure):
_fields_=[('vkCode',win.DWORD),
('scanCode',win.DWORD),
('flags',win.DWORD),
('time',win.DWORD),
('dwExtraInfo', ctypes.c_ulong)]
キーボードのどのキーが押されたかという情報は、KBDLLHOOKSTRUCT構造体のvkCode
に、仮想キーコードという形で格納されています。仮想キーコードはWindowsで定義されたキーボードの各キーを示す定数で、リファレンスにはその一覧があります。
vkCode
の値と仮想キーコード定数の比較を条件式に利用する、というのは一つの手ですが、キーボードのすべてのキーに対してそのような処理を書くのは現実的ではありません。
そこでwin32apiのToUnicode
関数を使って、仮想キーコードを翻訳します。フックプロシージャに組み込んだ例が次のコードです。
def hookProc(nCode, wParam, lParam):
if nCode == win32con.HC_ACTION and wParam == win32con.WM_KEYDOWN:
kb = KBDLLHOOKSTRUCT.from_address(lParam)
state = (ctypes.c_char * 256)()
user32.GetKeyboardState(ctypes.byref(state))
str = ctypes.create_unicode_buffer(8)
n = user32.ToUnicode(kb.vkCode, kb.scanCode, state, str, 8 - 1, 0)
if n > 0:
if kb.vkCode == win32con.VK_RETURN:
print()
else:
print(ctypes.wstring_at(str), end = "", flush = True)
return user32.CallNextHookEx(0, nCode, win.WPARAM(wParam), win.LPARAM(lParam))
ToUnicode
関数は、GetKeyboardState
関数が取得したキーボードの状態に応じて、仮想キーコードを翻訳します。shift
キーやnumlock
キーの状態を勘案してアルファベットの大文字小文字などの区別が可能になります。
残念ながらToUnicode
関数だけでは日本語の表現を得ることはできません。考えられる手はGetKeyboardState
関数で半角/全角
キーなどの、日本語入力のスイッチになるキーの状況を調べて、仮想キーコードを自力で翻訳するといったところでしょうか。
終わりに
最後はだいぶ駆け足になりましたが、まあなんとか使えそうなものを紹介できた気がするのでヨシとしておきます。前編の冒頭で触れように、pyhookというモジュールを使えば、多分ずっと安全にメッセージフックで遊ぶことができます。
参考
https://stackoverflow.com/questions/53732628/python-using-winapi-setwindowshookexa-on-windows-10
キーボードフックのフックプロシージャはこの記事から引用しました