Windowsでマウスの軌跡を表示するPythonスクリプト

### マウス軌跡の表示方法が違いすぎる

- ZOOMやMeetにはレーザーポインター機能はあるが
    - オンラインミーティングツールごとに違いがありすぎる
    - リアルなミーティングの際には使えない
- GoogleSlideにもレーザーポインタ機能があるが
    - 当然GoogleSlide以外の画面では通用しない
- PowerToysにもマウスを見やすくするツールはあるが
    - いろんなキーボードショートカット奪わすぎて不便

ということでしかたなく自作。

### どうせOS依存ならWin32APIをゴリゴリに叩けばいい

- Win32APIをゴリゴリに叩けるスクリプト言語ならPythonがちょうどいい
- PythonはCの関数を呼び出しやすい（なんならCよりも）

### 完成コード

```mouse.py
import signal
import sys
import tkinter as tk
from collections import deque
import ctypes
from ctypes import wintypes

class MouseTrailOverlay(tk.Tk):
    def __init__(self):
        super().__init__()
        
        # Windows API設定
        self.user32 = ctypes.windll.user32
        self._setup_windows_cursor()
        
        # GUI設定
        self.attributes('-fullscreen', True)
        self.attributes('-topmost', True)
        self.attributes('-transparentcolor', 'white')
        self.overrideredirect(True)
        
        # クリックスルー設定
        self._make_window_click_through()
        
        # 描画用キャンバス
        self.canvas = tk.Canvas(self, bg='white', highlightthickness=0)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.points = deque(maxlen=30)
        
        # 終了処理設定
        self.protocol("WM_DELETE_WINDOW", self.close)
        self._setup_signal_handlers()
        self._setup_timers()

    def _make_window_click_through(self):
        """ウィンドウをクリックスルー可能にする"""
        WS_EX_LAYERED = 0x00080000
        WS_EX_TRANSPARENT = 0x00000020
        hwnd = self.user32.GetParent(self.winfo_id())
        
        # 現在の拡張スタイルを取得
        ex_style = self.user32.GetWindowLongW(hwnd, -20)  # GWL_EXSTYLE=-20
        
        # 新しいスタイルを設定
        new_ex_style = ex_style | WS_EX_LAYERED | WS_EX_TRANSPARENT
        self.user32.SetWindowLongW(hwnd, -20, new_ex_style)
        self.user32.SetLayeredWindowAttributes(hwnd, 0x00FFFFFF, 255, 0x00000001)

    def _setup_windows_cursor(self):
        class POINT(ctypes.Structure):
            _fields_ = [('x', wintypes.LONG), ('y', wintypes.LONG)]
        
        def get_cursor_pos():
            pt = POINT()
            if self.user32.GetCursorPos(ctypes.byref(pt)):
                return pt.x, pt.y
            return 0, 0
        self.get_cursor_pos = get_cursor_pos

    def _setup_signal_handlers(self):
        signal.signal(signal.SIGINT, self._signal_handler)
        self.bind('<Control-c>', self._signal_handler)

    def _setup_timers(self):
        self.after(10, self._update_position)
        self.after(50, self._redraw_trail)
        self.after(100, self._check_exit)

    def _update_position(self):
        if hasattr(self, 'get_cursor_pos'):
            x, y = self.get_cursor_pos()
            self.points.append((x, y))
        self.after(10, self._update_position)

    def _redraw_trail(self):
        self.canvas.delete('trail')
        if len(self.points) > 1:
            self.canvas.create_line(
                *self.points, 
                fill='#FF0000', 
                width=5, 
                smooth=True, 
                tags='trail'
            )
        self.after(50, self._redraw_trail)

    def _check_exit(self):
        if not self.winfo_exists():
            sys.exit(0)
        self.after(100, self._check_exit)

    def _signal_handler(self, signum=None, frame=None):
        print("\n終了シグナルを受信しました")
        self.close()

    def close(self):
        self.destroy()
        self.quit()
        ctypes.windll.user32.PostQuitMessage(0)
        sys.exit(0)

if __name__ == '__main__':
    app = MouseTrailOverlay()
    try:
        app.mainloop()
    except KeyboardInterrupt:
        app.close()
```

### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/a7c69a8a-86d3-4d92-9093-0eacecec6f21.png)

CTL+c で終了します

### 解説

- tkinterモジュールで透明のウィンドウを最前面に表示
- win32apiでマウスの位置を取得
- 透明のウィンドウ上のcanvasに描画