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

　PythonからWin32apiを使ってWindowsの低レベルのマウスの入力イベントを監視する方法を紹介します。同様のことは[pyhook](https://pyhook.sourceforge.net/doc_1.5.0/ "pyhook")というモジュールを使ってもできるようです。
# 環境
* Windows11
* Python 3.11.0
* pywin32 306
# メッセージフックとは
　マイクロソフトが公開しているwin32apiのリファレンスには、[フックの概要](https://learn.microsoft.com/ja-jp/windows/win32/winmsg/about-hooks "フックの概要")として次のように説明されています。
> フックは、アプリケーションがメッセージ、マウスアクション、キーストロークなどのイベントをインターセプトできるメカニズムです。特定の種類のイベントをインターセプトする関数は、 フック プロシージャと呼ばれます。 フック プロシージャは、受け取る各イベントに対して動作し、イベントを変更または破棄できます。

 　ここで紹介する低レベルのマウスイベントのフックは、マウスアクションをWindowsのシステムが処理する前に、何らかの処理を割り込ませて情報を取得しようというものです。

:::note warn
注意
この処理はWindowsのシステムの文脈で行われるので、この処理の記述がまずいと最悪の場合システムにマウスアクションが正しく伝わらない事態もあり得ます。
:::
　メッセージフックにはwin32apiの三つの関数を使います。
* `SetWindowsHookExA()`
フックプロシージャをインストールする
* `CallNextHookEx()`
本来メッセージを処理するはずだったフックプロシージャにフック情報を渡す
* `UnhookWindowsHookEx()`
フックプロシージャをアンインストールする

　これらの関数はすべて`User32.dll`から利用できます。Pythonからはctypesモジュールを使えば利用することができます。
# ctypesの基本を簡単に
　ctypes は Python のための外部関数ライブラリです。ctypesモジュールを使えば、Pythonからの利用を想定されていないようなDLL（動的リンクライブラリ）を利用できます。
　ctypesの詳細については手に余るので、ここではメッセージフックに必要な最低限と思われることに絞って紹介します。
## DLLのロード
　ctypesを使ってDLLにアクセスする方法はいくつかありますが、ここではCDLL系クラスのインスタンスを利用する方法を紹介します。
　CDLL系のクラスはコンストラクタに「.dll」ファイルをパスで指定するだけで、DLLをインスタンスとしてロードします。このインスタンスを介して、インポートしたモジュールのようにDLLを利用できるようになります。
　CDLL系クラスは、対象になるDLLのスタイルに応じたものを選ぶ必要があります。win32apiのDLLには`WinDLL`クラスを使います。
``` python sample01.py
import ctypes
user32 = ctypes.WinDLL('user32', use_last_error=True)
print(user32.SetWindowsHookExA)
```
```
<_FuncPtr object at 0x0000021825D79A50>
```

　一見パスを指定しているようには見えませんが、これはWindowsの環境変数の都合でファイル名だけの記述でパスとして通じています。二番目の引数はエラーメッセージに関するもので、エラーメッセージを利用する気がないのならあえて`True`にセットする必要はないと思います。

## DLL関数に渡す引数
　DLLの関数に渡す値は、ctypesモジュールで定義された互換型を利用します。基本的にはctypesのマニュアルにある[基本データ型](https://docs.python.org/ja/3/library/ctypes.html#fundamental-data-types '基本データ型')の一覧表から、対応するものを選ぶ形になります。
　win32apiのDLLを扱う場合では`ctypes.wintypes`モジュールが便利です。`wintypes`にはwin32apiで利用される様々な型の互換型が、まったく同じ名前で定義されています。win32apiのDLL関数に渡す値は、apiのリファレンスで指定されているものと同じ名前の互換型で与えれば間違いないでしょう。
## DLL関数からの戻り値
　DLL関数から得られる戻り値は、互換型ではなくPythonの基本型として得られます。この点で注意が必要なのは、DLL関数の出力をすぐに別のDLL関数に渡したい、という時でも互換型への変換が必要になるということです。
## 構造体と関数の互換型
　メッセージフックを行う上で必要になる、構造体とコールバック関数の互換型について少し詳しく書きます。
### 構造体
　構造体はc言語などにあるクラスに似た仕組みです。構造体はいくつかの変数のまとまりからなる型として、自由に定義できるものです。マウスアクションのフックで取得できるデータは`tagMSLLHOOKSTRUCT`という構造体として得られます。次のように定義されています。
```c++
typedef struct tagMSLLHOOKSTRUCT {
  POINT     pt;
  DWORD     mouseData;
  DWORD     flags;
  DWORD     time;
  ULONG_PTR dwExtraInfo;
} MSLLHOOKSTRUCT, *LPMSLLHOOKSTRUCT, *PMSLLHOOKSTRUCT;
``` 

　この構造体は「POINT型の変数」を一つ、「DWORD型の変数」を三つ、「ULONG_PTR型の変数」を一つ、計５つの変数からなるひとつの型になります。
　Pythonでこのような構造体のデータを受け取るためには、ctypesの`Structure`クラスの継承クラスを利用します。

~~~python sample02.py
import ctypes
import ctypes.wintypes as win
class MsStructuer(ctypes.Structure):#ctypes.Structureクラスの継承
    _fields_ = [('point', win.POINT),
                ('mouseDate', win.DWORD),
                ('flags', win.DWORD),
                ('time', win.DWORD),
                ('dwExtraInfo', ctypes.c_ulong)]
~~~

　このように`_fields_`属性に、対象になる構造体の要素を「名前（任意）」と「型」からなる「タプル」のリストで指定することで、特定の構造体に対するPythonの互換クラスを作成できます。
　ここで指定する要素の名前は、各要素にアクセスするためのPythonクラスの属性の名前になります。また、型は互換型で指定しますが、`POINT`も`DWORD`も`ctypes.wintypes`にあるものをそのまま利用できます。
### コールバック関数
　`SetWindowsHookExA`関数にはコールバック関数として、Pythonで記述したフックプロシージャを渡すことになります。しかし、他の変数同様にPythonの関数オブジェクトをそのまま`Set~`関数に渡すことはできません。互換性のある方に変換して渡す必要があります。
　マウスアクションのフックで`SetWindowsHookExA`関数が期待しているコールバック関数は次のようなものです。
~~~c++
LRESULT CALLBACK LowLevelMouseProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);
~~~

　これは「LRESULT型」を返す「int型」と「WPARAM型」と「LPARAM型」を引数にとる関数の定義です。Pythonで記述されるフックプロシージャはまず、三つの引数を受け取って一つの値を返す、という要件を満たしている必要があります。
　Pythonの関数オブジェクトを上のような関数と互換性のある形に変換するには、まず`ctypes.WINFUNCTYPE`関数を使って`WinFunctionType`クラスを作成します。
~~~python sample03.py
HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_int, win.WPARAM, win.LPARAM)
~~~

　`ctypes.WINFUNCTYPE`関数には、目的のコールバック関数の「戻り値の型」と「引数の型」を指定します。戻り値である「LRESULT型」は、win32apiによくある「long型の整数値」によるなんらかのメッセージの表現です。`wintypes`に「LRESULT型」がなかったので`c_long`で代用しました。
　この関数の戻り値として得られた`WinFunctionType`クラスに、Pythonの関数オブジェクトを渡してインスタンスを作ると、ようやくコールバック関数の互換オブジェクトが得られます。
~~~python sample04.py
def func(nCode, wParam, lParam):
    return 0
HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_int, win.WPARAM, win.LPARAM)
callback = HOOKPROC(func)
~~~

　この`callback`であれば`SetWindowsHookExA`関数は受け取ってくれます。
# Set~、Call~、Unhook~関数の詳細
　あとはSet~、Call~、Unhook~の使い方と、フックプロシージャの詳細を詰めれば、マウスフックのPythonスクリプトに進むことができます。もうちょっとです。
　`SetWindowsHookExA`は以下のように定義されています。
~~~c++
HHOOK SetWindowsHookExA(
  [in] int       idHook,
  [in] HOOKPROC  lpfn,
  [in] HINSTANCE hmod,
  [in] DWORD     dwThreadId
);
~~~
　引数の意味するところはだいたい以下のような感じです。
* idHook
整数値の識別子でもって、フックの種類を指定
* lpfn
フックプロシージャのポインタを指定。上で作った`callback`を渡す部分です
* hmod
フックプロシージャを含むDLLのハンドルを指定。フックプロシージャが`Set~`関数の呼び出し元と同じコード内に記述されている場合は`null`、つまりPythonでは`None`を指定します
* dwThreadId
フックプロシージャを関連付けるスレッドの整数値による識別子。0を指定することですべてのスレッドが対象になります
* 戻り値：HHOOK
関数が成功した場合「HHOOK型」によるフックプロシージャへのハンドルを、失敗した場合は`null`を返す。この「HHOOK型」は`UnhookWindowsHookEx`の引数として利用するので、大切に保管しましょう

　つづいて`CallNextHookEx`の定義です。
~~~c++
LRESULT CallNextHookEx(
  [in, optional] HHOOK  hhk,
  [in]           int    nCode,
  [in]           WPARAM wParam,
  [in]           LPARAM lParam
);
~~~
* hhk
[リファレンス](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-callnexthookex?source=recommendations 'CallNextHookEx')によると、この引数は無視されます、とのことなのでなんでもいいです
* nCode, wParam, lParam
これらの引数にはフックプロシージャが受け取る引数をそのまま渡してやります
* 戻り値：LRESULT
フックプロシージャはこの戻り値を返す必要がある

　つまり`CallNextHookEx`は、フックプロシージャの中で呼び出す必要があるということです。フックプロシージャの`return`の部分で`CallNextHookEx`を呼び出すのがセオリーのようです。
　`UnhookWindowsHookEx`です。
```c++
BOOL UnhookWindowsHookEx(
  [in] HHOOK hhk
);
```
* hhk
`Set~`の返した値を渡すことで、`Set~`がインストールしたフックプロシージャがアンインストールされます
* 戻り値：BOOL
フックプロシージャのアンインストールの成否を表すものです

　最後にフックプロシージャの受け取る三つの引数についてです。
* nCode
整数値による識別子。wParamとlParamがマウスメッセージに関する情報を持っていれば「0」。「0」はHC_ACTIONという定数として定義されている
* wParam
WPARAM型によるマウスメッセージの識別子。WM_LBUTTONDOWN、WM_LBUTTONUP、WM_MOUSEMOVE、WM_MOUSEWHEEL、WM_RBUTTONDOWN、M_RBUTTONUPというWPARAM型の定数として定義されている

* lParam
MSLLHOOKSTRUCT 構造体へのポインタ。構造体そのものではなく、その位置だけが示される形なので、lParamから構造体の情報を取得するには`Structure`クラスの`from_address`関数を使います

　PythonからnCodeとwParam用の定数として定義された値は、pywin32に含まれるwin32conというモジュールから利用できます。`SetWindowsHookExA`の`idHook`で指定するフック識別子も同様です。
# Pythonによるマウスフックスクリプト
　以上のことを押さえておけば、次のPythonスクリプトがおおむね理解できるかと思います。マウスの左クリックに応じて、アクションの発生した時間と画面上での座標が、逐次コンソールウィンドウに`print`されるというものです。
　
```python sample04.py
import win32con
import ctypes
import ctypes.wintypes as win

user32 = ctypes.WinDLL('user32', use_last_error=True)

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.lUser32 = user32
        self.hooked = None

    def installHookProc(self,pointer):
        self.hooked = self.lUser32.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.lUser32.UnhookWindowsHookEx(self.hooked)
        self.hooked = None

def hookProc(nCode, wParam, lParam):
    if nCode == win32con.HC_ACTION and wParam == win32con.WM_LBUTTONDOWN:
        ms = MsStructuer.from_address(lParam)
        print(f'{ms.time}:{ms.point.x}, {ms.point.y}')#timeはミリ秒単位かな
    if nCode == win32con.HC_ACTION and wParam == win32con.WM_RBUTTONDOWN:
        print('右クリックが押されました。フックを解除します')
        kl.uninstalHookProc()
        return user32.CallNextHookEx(0, nCode, win.WPARAM(wParam), win.LPARAM(lParam))
    return user32.CallNextHookEx(0, nCode, win.WPARAM(wParam), win.LPARAM(lParam))

HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_int, win.WPARAM, win.LPARAM)
callback = HOOKPROC(hookProc)

kl = KeyLogger()
bool = kl.installHookProc(callback)
msg = win.MSG()
user32.GetMessageA(ctypes.byref(msg), 0, 0, 0)
```
　最後の二行、win32apiの`GetMessageA`関数を呼び出すための記述は、Pythonスクリプトを終了させないための、とりあえずのものです。なぜそのような措置が必要なのかは、メッセージフックにおいてPythonスクリプトが果たしている役割について注目するとわかります。

　このPythonスクリプトがやっているのは、フックプロシージャや各クラスの定義以外の部分では、KeyLoggerクラスのインスタンスを作成して、フックプロシージャをインストールしているだけです。フックプロシージャを実行するのはWindowsのシステムなので、Pythonスクリプトの役割はこれ以上なにもないからです。
　しかし、やることがないからといってスクリプトに終了してもらうわけにはいきません。スクリプトが終了してしまえば、フックプロシージャの記述が失われてしまい、自動的にフックが解除されてしまうからです。

　`GetMessageA`関数は、指定したウィンドウにメッセージが届くのを「待機」する関数です。この場合は、届くはずのないメッセージを待ち続ける形で、Pythonスクリプトの終了を防いでいます。しかし、これではPythonインタプリタを停止させる以外スクリプトを止めるすべがないので、あくまでも「とりあえず」の手段です。
# 終わりに
　あまりに長くなりすぎたので、フックスクリプトが完成したところで、とりあえず、この記事を前半ということにしてまとめることにします。
　最後に示したスクリプトでは、行儀よく終了させる手段がないとか、フックプロシージャが取得した情報を活用する手段がないなど、いろいろと問題があります。続きの記事でこれらの問題をクリアした例と、低レベルのキーボードメッセージへの応用例を紹介したいと思います。
# 参考
* https://stackoverflow.com/questions/53732628/python-using-winapi-setwindowshookexa-on-windows-10
最後のスクリプトはこの質問への回答を参考にしています
* https://stackoverflow.com/questions/38557655/python-windows-mouse-hook-crash
DLLのロードについて、この質問への回答に準じました
* http://www.wisdomsoft.jp/
windowsapiについて検索すると、こちらの解説が結構な頻度でヒットします。公式のリファレンスで詰まった時には大変頼りになります