マウス軌跡の表示方法が違いすぎる
- 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()
実行結果
CTL+c で終了します
解説
- tkinterモジュールで透明のウィンドウを最前面に表示
- win32apiでマウスの位置を取得
- 透明のウィンドウ上のcanvasに描画