PythonによるWindowsメッセージフック・後編

　[PythonによるWindowsメッセージフック・前編](https://qiita.com/Aroe/items/502cc5665ae17db8654a)の続きです。前編ではクリックに応じてコンソールウィンドウに、クリックが発生した時間と画面上の座標を`print`するPythonスクリプトを作成しましたが、次のような問題のあるものでした。
* Pythonスクリプトの範囲で正しく終了できない
* フックで取得した情報をPythonスクリプト側で利用できない

　後編のこの記事では、まず以上の問題点を解決した実用にかなう、マウスアクションをフックするPythonスクリプトの例を示したいと思います。
# 改修案
　新しいフックスクリプトを次のように変更します。
* スクリプトの終了を防ぐために、`GetMessageA`の代わりにPySimpleGUIのメッセージループを使う
PySimpleGUIの`read()`関数も`GetMessageA`のようにメッセージを待機する関数なので、スクリプトの終了を防ぐことができます
* フックプロシージャをKeyLoggerクラスに統合する
クラスに属する関数として定義することで、フックプロシージャが`self`を介して属性にアクセスすることができます。Python側からはKeyLoggerクラスのインスタンスの属性を参照すればフックプロシージャからの情報が取得できます

# マウスフックスクリプト
　新しいスクリプトは以下のようになります。
```python sample01.py
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にも同様のものが定義されています。
```python sample02.py
SetWindowsHookExA(win32con.WH_KEYBOARD_LL, callback, None, 0)
```

　コールバック関数は次のように定義されています。
```c++
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`関数を使って情報を取得します。
```python sample03.py
class KBDLLHOOKSTRUCT(ctypes.Structure):
    _fields_=[('vkCode',win.DWORD),
              ('scanCode',win.DWORD),
              ('flags',win.DWORD),
              ('time',win.DWORD),
              ('dwExtraInfo', ctypes.c_ulong)]
```

　キーボードのどのキーが押されたかという情報は、KBDLLHOOKSTRUCT構造体の`vkCode`に、仮想キーコードという形で格納されています。仮想キーコードはWindowsで定義されたキーボードの各キーを示す定数で、リファレンスにはその[一覧](https://learn.microsoft.com/ja-jp/windows/win32/inputdev/virtual-key-codes)があります。
　`vkCode`の値と仮想キーコード定数の比較を条件式に利用する、というのは一つの手ですが、キーボードのすべてのキーに対してそのような処理を書くのは現実的ではありません。
　そこでwin32apiの`ToUnicode`関数を使って、仮想キーコードを翻訳します。フックプロシージャに組み込んだ例が次のコードです。
```python sample04.py
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://sourceforge.net/p/pyhook/wiki/PyHook_Tutorial/)というモジュールを使えば、多分ずっと安全にメッセージフックで遊ぶことができます。
# 参考
https://stackoverflow.com/questions/53732628/python-using-winapi-setwindowshookexa-on-windows-10
キーボードフックのフックプロシージャはこの記事から引用しました