PRA-only-demo/contrib/windows-runner-actuate/native/NativeInput.cs

213 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Runtime.InteropServices;
namespace AgentWorker.WindowsRunner;
// 输入类 P/Invoke 独立成类:NativeWindows 保持对面"只读编译边界"原样不动。
// 本层没有进程启动接口;所有输入只作用于经策略核验后的前台窗口。
// 坐标一律物理屏幕像素——进程入口(ActuateService.EnsureDpiAwareness)声明
// Per-Monitor V2 DPI 感知,与 RPA 侧 PowerShell 驱动每脚本声明的
// SetProcessDpiAwarenessContext(-4) 语义一致,保证 SetCursorPos 全程物理像素。
// 抢前台五招兜底从 RPA 驱动 activateWindowScript 逐行移植:
// Windows 前台锁定策略会拒绝普通 SetForegroundWindow(应用点击"运行"后自身是前台),
// 五招顺序与睡眠时间是踩坑调出来的实战参数,一行语义都不允许改动。
internal static class NativeInput
{
private const uint InputMouse = 0;
private const uint MouseEventfMove = 0x0001;
private const uint MouseEventfLeftDown = 0x0002;
private const uint MouseEventfLeftUp = 0x0004;
private const uint MouseEventfRightDown = 0x0008;
private const uint MouseEventfRightUp = 0x0010;
private const uint MouseEventfWheel = 0x0800;
internal const int SwRestore = 9;
internal const int SwMinimize = 6;
internal const int SwMaximize = 3;
private const uint WmClose = 0x0010;
private const byte VkMenu = 0x12; // ALT
private const uint KeyeventfKeyup = 0x0002;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out Point point);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetForegroundWindow(nint handle);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ShowWindow(nint handle, int command);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsIconic(nint handle);
[DllImport("user32.dll")]
internal static extern void keybd_event(byte virtualKey, byte scanCode, uint flags, nint extraInfo);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AttachThreadInput(uint currentThread, uint targetThread, bool attach);
[DllImport("user32.dll")]
private static extern void SwitchToThisWindow(nint handle, bool altTab);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetProcessDpiAwarenessContext(nint value);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(nint handle, nint after, int x, int y,
int width, int height, uint flags);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PostMessage(nint handle, uint message, nint wParam, nint lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint count, Input[] inputs, int size);
[StructLayout(LayoutKind.Sequential)]
internal struct Point { public int X; public int Y; }
[StructLayout(LayoutKind.Sequential)]
private struct Input
{
public uint Type;
public InputUnion Union;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)] public MouseInput Mouse;
}
[StructLayout(LayoutKind.Sequential)]
private struct MouseInput
{
public int Dx;
public int Dy;
public uint MouseData;
public uint Flags;
public uint Time;
public nint ExtraInfo;
}
// DPI 感知声明是进程级一次性设置;失败(如已被宿主先行声明)不影响后续调用。
internal static void EnsureDpiAwareness()
{
try { _ = SetProcessDpiAwarenessContext(new nint(-4)); }
catch { /* 旧系统无此 API 时按物理像素继续;对面 CoordinateSpace 以 GetDpiForWindow 实测为准。 */ }
}
// SendInput 批量提交鼠标标志组合;单次失败按 input_failed 处理由上层转稳定错误码。
internal static void SendMouse(uint flags, uint data)
{
var input = new Input
{
Type = InputMouse,
Union = new InputUnion
{
Mouse = new MouseInput { Dx = 0, Dy = 0, MouseData = data, Flags = flags }
}
};
if (SendInput(1, [input], Marshal.SizeOf<Input>()) != 1)
throw new RunnerException("input_failed");
}
internal static void SendWheel(int amount) =>
SendMouse(MouseEventfWheel, unchecked((uint)amount));
internal static void Click(bool right, int repeats, int settleMs)
{
var down = right ? MouseEventfRightDown : MouseEventfLeftDown;
var up = right ? MouseEventfRightUp : MouseEventfLeftUp;
for (var i = 0; i < repeats; i++)
{
SendMouse(down, 0);
SendMouse(up, 0);
if (settleMs > 0) Thread.Sleep(settleMs);
}
}
internal static Point CursorPosition()
{
if (!GetCursorPos(out var point)) throw new RunnerException("input_failed");
return point;
}
// 窗口拖拽时序:按下→hold→移动→release→抬起,与 RPA 驱动逐段一致。
internal static void DragSequence(bool right, int fromX, int fromY, int toX, int toY,
int holdMs, int releaseDelayMs)
{
var down = right ? MouseEventfRightDown : MouseEventfLeftDown;
var up = right ? MouseEventfRightUp : MouseEventfLeftUp;
if (!SetCursorPos(fromX, fromY)) throw new RunnerException("input_failed");
SendMouse(down, 0);
if (holdMs > 0) Thread.Sleep(holdMs);
if (!SetCursorPos(toX, toY)) throw new RunnerException("input_failed");
if (releaseDelayMs > 0) Thread.Sleep(releaseDelayMs);
SendMouse(up, 0);
}
// 抢前台五招兜底(RPA 驱动 activateWindowScript 的逐行移植):
// ①最小化则恢复 +150ms ②SwitchToThisWindow +150ms ③ALT 借位 + SetForegroundWindow +200ms
// ④AttachThreadInput 挂到前台线程 + SetForegroundWindow +200ms ⑤最小化再恢复 +450ms
// 五招全失败抛 focus_failed——对应 PS 版 RPA_ERROR '无法把窗口带到前台'。
internal static void FocusChain(nint handle)
{
if (IsIconic(handle))
{
_ = ShowWindow(handle, SwRestore);
Thread.Sleep(150);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
SwitchToThisWindow(handle, true);
Thread.Sleep(150);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
keybd_event(VkMenu, 0, 0, nint.Zero);
_ = SetForegroundWindow(handle);
keybd_event(VkMenu, 0, KeyeventfKeyup, nint.Zero);
Thread.Sleep(200);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
// 返回值即前台窗口的线程号;PID 出参不需要。
var foregroundThread = NativeWindows.GetWindowThreadProcessId(
NativeWindows.GetForegroundWindow(), out _);
var currentThread = GetCurrentThreadId();
var attached = AttachThreadInput(currentThread, foregroundThread, true);
try { _ = SetForegroundWindow(handle); }
finally { if (attached) _ = AttachThreadInput(currentThread, foregroundThread, false); }
Thread.Sleep(200);
}
if (NativeWindows.GetForegroundWindow() != handle)
{
_ = ShowWindow(handle, SwMinimize);
Thread.Sleep(150);
_ = ShowWindow(handle, SwRestore);
Thread.Sleep(300);
}
if (NativeWindows.GetForegroundWindow() != handle)
throw new RunnerException("focus_failed");
}
internal static void MoveWindowBounds(nint handle, int x, int y, int width, int height) =>
_ = SetWindowPos(handle, nint.Zero, x, y, width, height, 4); // flags=4 SWP_NOZORDER,与 RPA 驱动一致
internal static void CloseWindow(nint handle) =>
_ = PostMessage(handle, WmClose, nint.Zero, nint.Zero);
}