using System.Runtime.InteropServices; using System.Text; namespace anjianjingling.Capture { /// /// 操作录制器:WH_KEYBOARD_LL / WH_MOUSE_LL 低级钩子采集真实键鼠事件, /// 结束时聚合为可直接回放的 Lua 脚本。 /// public sealed class InputRecorder { // ===================== Win32 ===================== private const int WH_KEYBOARD_LL = 13; private const int WH_MOUSE_LL = 14; private const int WM_KEYDOWN = 0x0100, WM_KEYUP = 0x0101; private const int WM_SYSKEYDOWN = 0x0104, WM_SYSKEYUP = 0x0105; private const int WM_MOUSEMOVE = 0x0200; private const int WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202; private const int WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205; private const int WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208; private const int WM_MOUSEWHEEL = 0x020A; private const int WM_QUIT = 0x0012; private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential)] private struct KBDLLHOOKSTRUCT { public uint vkCode; public uint scanCode; public uint flags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; } [StructLayout(LayoutKind.Sequential)] private struct MSLLHOOKSTRUCT { public POINT pt; public uint mouseData; public uint flags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public POINT pt; } [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll")] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); [DllImport("user32.dll")] private static extern bool TranslateMessage(ref MSG lpMsg); [DllImport("user32.dll")] private static extern IntPtr DispatchMessage(ref MSG lpMsg); [DllImport("user32.dll")] private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? lpModuleName); [DllImport("user32.dll")] private static extern bool GetKeyboardState(byte[] lpKeyState); [DllImport("user32.dll")] private static extern short GetKeyState(int nVirtKey); [DllImport("user32.dll")] private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl); [DllImport("user32.dll")] private static extern IntPtr GetKeyboardLayout(uint idThread); [DllImport("user32.dll")] private static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[]? lpList); // ===================== 事件模型 ===================== private enum Kind { Move, ButtonDown, ButtonUp, Wheel, KeyDown, KeyUp } private sealed class RecEvent { public long T; public Kind Kind; public int X, Y; public string Btn = "left"; public int Vk; public string KeyName = ""; public string? Ch; public int Wheel; public bool IsPrintable => Ch is { Length: > 0 }; } private readonly List _events = new(); private readonly object _lock = new(); private long _t0; private Thread? _thread; private uint _threadId; private IntPtr _kbHook, _msHook; private HookProc? _kbProc, _msProc; private readonly ManualResetEventSlim _ready = new(false); private volatile bool _recording; public bool IsRecording => _recording; public event Action? StateChanged; /// 录制鼠标移动轨迹(关闭后只记录点击)。 public bool RecordMouseMove { get; set; } = true; /// 把连续可打印字符聚合成 typeText("...")。 public bool SmartText { get; set; } = true; /// 生成脚本时单次延时上限(毫秒),超过则截断。 public int MaxDelayMs { get; set; } = 2000; /// 录制期间忽略的虚拟键(默认吞掉本程序热键 F8~F12)。 public HashSet IgnoredVk { get; } = new() { 0x77, 0x78, 0x79, 0x7A, 0x7B }; // ===================== 开始 / 停止 ===================== public void Start() { if (_recording) return; lock (_lock) _events.Clear(); _t0 = Environment.TickCount64; _recording = true; _thread = new Thread(HookThread) { IsBackground = true, Name = "InputRecorderHook" }; _thread.SetApartmentState(ApartmentState.STA); _thread.Start(); _ready.Wait(2000); StateChanged?.Invoke(true); } private void HookThread() { _threadId = GetCurrentThreadId(); _kbProc = KbProc; _msProc = MsProc; IntPtr hMod = GetModuleHandle(null); _kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hMod, 0); _msHook = SetWindowsHookEx(WH_MOUSE_LL, _msProc, hMod, 0); _ready.Set(); while (GetMessage(out MSG msg, IntPtr.Zero, 0, 0) > 0) { TranslateMessage(ref msg); DispatchMessage(ref msg); } if (_kbHook != IntPtr.Zero) UnhookWindowsHookEx(_kbHook); if (_msHook != IntPtr.Zero) UnhookWindowsHookEx(_msHook); _kbHook = _msHook = IntPtr.Zero; } private IntPtr KbProc(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && _recording) { try { var kb = Marshal.PtrToStructure(lParam); int msg = wParam.ToInt32(); bool down = msg is WM_KEYDOWN or WM_SYSKEYDOWN; bool up = msg is WM_KEYUP or WM_SYSKEYUP; if ((down || up) && !IgnoredVk.Contains((int)kb.vkCode)) { var e = new RecEvent { T = Environment.TickCount64 - _t0, Kind = down ? Kind.KeyDown : Kind.KeyUp, Vk = (int)kb.vkCode, KeyName = KeyName((int)kb.vkCode) }; if (down) { var ch = ToChar((int)kb.vkCode, (int)kb.scanCode); if (ch.HasValue && !CtrlAltDown()) e.Ch = ch.Value.ToString(); } lock (_lock) _events.Add(e); } } catch { } } return CallNextHookEx(_kbHook, nCode, wParam, lParam); } private IntPtr MsProc(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && _recording) { try { var ms = Marshal.PtrToStructure(lParam); int msg = wParam.ToInt32(); var e = new RecEvent { T = Environment.TickCount64 - _t0, X = ms.pt.X, Y = ms.pt.Y }; switch (msg) { case WM_MOUSEMOVE: if (!RecordMouseMove) break; e.Kind = Kind.Move; lock (_lock) { // 采样节流:同位置或 15ms 内的移动丢弃 if (_events.Count > 0) { var last = _events[^1]; if (last.Kind == Kind.Move) { if (e.T - last.T < 15 && Math.Abs(e.X - last.X) < 3 && Math.Abs(e.Y - last.Y) < 3) break; } } _events.Add(e); } break; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: e.Kind = Kind.ButtonDown; e.Btn = msg == WM_RBUTTONDOWN ? "right" : msg == WM_MBUTTONDOWN ? "middle" : "left"; lock (_lock) _events.Add(e); break; case WM_LBUTTONUP: case WM_RBUTTONUP: case WM_MBUTTONUP: e.Kind = Kind.ButtonUp; e.Btn = msg == WM_RBUTTONUP ? "right" : msg == WM_MBUTTONUP ? "middle" : "left"; lock (_lock) _events.Add(e); break; case WM_MOUSEWHEEL: e.Kind = Kind.Wheel; e.Wheel = (short)((ms.mouseData >> 16) & 0xFFFF) / 120; lock (_lock) _events.Add(e); break; } } catch { } } return CallNextHookEx(_msHook, nCode, wParam, lParam); } /// 停止录制并生成 Lua 脚本。 public string Stop() { if (!_recording) return ""; _recording = false; StateChanged?.Invoke(false); if (_threadId != 0) PostThreadMessage(_threadId, WM_QUIT, IntPtr.Zero, IntPtr.Zero); _thread?.Join(1500); _thread = null; _threadId = 0; List snapshot; lock (_lock) snapshot = new List(_events); return Generate(snapshot); } public void Abort() { if (!_recording) return; _recording = false; StateChanged?.Invoke(false); if (_threadId != 0) PostThreadMessage(_threadId, WM_QUIT, IntPtr.Zero, IntPtr.Zero); _thread?.Join(1500); _thread = null; _threadId = 0; lock (_lock) _events.Clear(); } // ===================== 键名 / 字符 ===================== private static bool CtrlAltDown() => (GetKeyState(0x11) & 0x8000) != 0 || (GetKeyState(0x12) & 0x8000) != 0; private static char? ToChar(int vk, int scan) { try { var state = new byte[256]; if (!GetKeyboardState(state)) return null; var sb = new StringBuilder(8); var layout = GetKeyboardLayout(0); int n = ToUnicodeEx((uint)vk, (uint)scan, state, sb, sb.Length, 0, layout); if (n == 1 && sb.Length > 0) { char c = sb[0]; if (c >= 32 && c != 127) return c; } } catch { } return null; } private static readonly Dictionary VkNames = new() { [0x0D] = "ENTER", [0x1B] = "ESC", [0x20] = "SPACE", [0x09] = "TAB", [0x08] = "BACKSPACE", [0x2E] = "DELETE", [0x2D] = "INSERT", [0x24] = "HOME", [0x23] = "END", [0x21] = "PAGEUP", [0x22] = "PAGEDOWN", [0x25] = "LEFT", [0x26] = "UP", [0x27] = "RIGHT", [0x28] = "DOWN", [0x11] = "CTRL", [0x12] = "ALT", [0x10] = "SHIFT", [0x5B] = "WIN", [0x14] = "CAPSLOCK", [0x90] = "NUMLOCK", [0x6A] = "MULTIPLY", [0x6B] = "ADD", [0x6D] = "SUBTRACT", [0x6E] = "DECIMAL", [0x6F] = "DIVIDE", [0xBA] = "SEMICOLON", [0xBB] = "EQUAL", [0xBC] = "COMMA", [0xBD] = "MINUS", [0xBE] = "PERIOD", [0xBF] = "SLASH", [0xC0] = "BACKQUOTE", [0xDB] = "LBRACKET", [0xDC] = "BACKSLASH", [0xDD] = "RBRACKET", [0xDE] = "QUOTE", }; private static string KeyName(int vk) { if (VkNames.TryGetValue(vk, out var n)) return n; if (vk >= 0x70 && vk <= 0x87) return "F" + (vk - 0x6F); if (vk >= 0x60 && vk <= 0x69) return "NUMPAD" + (vk - 0x60); if (vk >= 0x30 && vk <= 0x39) return ((char)vk).ToString(); if (vk >= 0x41 && vk <= 0x5A) return ((char)vk).ToString(); return "VK" + vk.ToString("X2"); } private static bool IsModifier(int vk) => vk is 0x11 or 0x12 or 0x10 or 0x5B or 0x5C or 0xA0 or 0xA1 or 0xA2 or 0xA3 or 0xA4 or 0xA5; private static string ModName(int vk) => vk switch { 0x11 or 0xA2 or 0xA3 => "Ctrl", 0x12 or 0xA4 or 0xA5 => "Alt", 0x10 or 0xA0 or 0xA1 => "Shift", 0x5B or 0x5C => "Win", _ => "" }; // ===================== Lua 生成 ===================== private string Generate(List ev) { var sb = new StringBuilder(); sb.AppendLine("-- 由操作录制生成(" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ")"); sb.AppendLine("-- 修改坐标/延时后可直接运行;模板图片放 images/ 目录"); sb.AppendLine(); long lastT = 0; var mods = new List(); var textBuf = new StringBuilder(); int i = 0; void FlushText() { if (textBuf.Length == 0) return; sb.AppendLine($"typeText(\"{Escape(textBuf.ToString())}\")"); textBuf.Clear(); } void EmitDelay(long t) { long gap = t - lastT; if (gap < 30) return; if (gap > MaxDelayMs) gap = MaxDelayMs; FlushText(); sb.AppendLine($"delay({gap})"); lastT = t; } while (i < ev.Count) { var e = ev[i]; switch (e.Kind) { case Kind.Move: { // 合并连续移动,只保留最终落点 int j = i; while (j + 1 < ev.Count && ev[j + 1].Kind == Kind.Move) j++; var end = ev[j]; EmitDelay(e.T); sb.AppendLine($"moveTo({end.X}, {end.Y})"); lastT = end.T; i = j + 1; continue; } case Kind.ButtonDown: { // 找 500ms 内的同名弹起 → 单击 int j = i + 1; RecEvent? up = null; while (j < ev.Count) { if (ev[j].Kind == Kind.ButtonUp && ev[j].Btn == e.Btn && ev[j].T - e.T < 500) { up = ev[j]; break; } if (ev[j].Kind == Kind.ButtonDown) break; j++; } EmitDelay(e.T); string fn = e.Btn switch { "right" => "rightClick", "middle" => "middleClick", _ => "leftClick" }; if (up != null) { sb.AppendLine($"{fn}({e.X}, {e.Y})"); lastT = up.T; i = j + 1; continue; } sb.AppendLine($"moveTo({e.X}, {e.Y})"); sb.AppendLine($"mouse.down(\"{e.Btn}\")"); i++; continue; } case Kind.ButtonUp: EmitDelay(e.T); sb.AppendLine($"mouse.up(\"{e.Btn}\")"); i++; continue; case Kind.Wheel: EmitDelay(e.T); sb.AppendLine($"wheel({e.Wheel})"); i++; continue; case Kind.KeyDown: { if (IsModifier(e.Vk)) { string m = ModName(e.Vk); if (m.Length > 0 && !mods.Contains(m)) mods.Add(m); i++; continue; } // 找对应弹起 int j = i + 1; while (j < ev.Count && !(ev[j].Kind == Kind.KeyUp && ev[j].Vk == e.Vk)) j++; bool paired = j < ev.Count; if (mods.Count > 0 && mods.Any(m => m is "Ctrl" or "Alt" or "Win")) { EmitDelay(e.T); sb.AppendLine($"hotkey(\"{string.Join("+", mods.Append(e.KeyName))}\")"); // 跳过本次按下到弹起之间的所有事件 i = paired ? j + 1 : i + 1; lastT = paired ? ev[j].T : e.T; continue; } if (SmartText && e.IsPrintable) { textBuf.Append(e.Ch); i = paired ? j + 1 : i + 1; lastT = paired ? ev[j].T : e.T; continue; } EmitDelay(e.T); sb.AppendLine($"keyPress(\"{e.KeyName}\")"); i = paired ? j + 1 : i + 1; lastT = paired ? ev[j].T : e.T; continue; } case Kind.KeyUp: // 修饰键弹起 if (IsModifier(e.Vk)) { string m = ModName(e.Vk); if (m.Length > 0) mods.Remove(m); i++; continue; } EmitDelay(e.T); sb.AppendLine($"keyUp(\"{e.KeyName}\")"); i++; continue; } i++; } FlushText(); return sb.ToString(); } private static string Escape(string s) => s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "").Replace("\n", "\\n"); } }