using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Runtime.InteropServices; using Forms = System.Windows.Forms; namespace JJServiceManager.Remote { /// 모니터 정보 (실제 픽셀 좌표) public sealed class MonitorInfo { public int Index { get; set; } public string Name { get; set; } = ""; public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } public bool Primary { get; set; } } /// 대상 PC: 화면 캡처 public static class ScreenCapture { /// 모니터 목록 (1부터, 주 모니터 우선이 아니라 Windows 순서) public static List Monitors() { var list = new List(); int i = 1; foreach (var s in Forms.Screen.AllScreens.OrderBy(s => s.Bounds.X).ThenBy(s => s.Bounds.Y)) { list.Add(new MonitorInfo { Index = i++, Name = s.DeviceName, X = s.Bounds.X, Y = s.Bounds.Y, Width = s.Bounds.Width, Height = s.Bounds.Height, Primary = s.Primary }); } return list; } /// monitor 0 = 전체 화면, 1~ = 해당 모니터 public static Rectangle Area(int monitor) { if (monitor > 0) { var m = Monitors().FirstOrDefault(x => x.Index == monitor); if (m != null) return new Rectangle(m.X, m.Y, m.Width, m.Height); } return Forms.SystemInformation.VirtualScreen; } [StructLayout(LayoutKind.Sequential)] private struct CURSORINFO { public int cbSize; public int flags; public IntPtr hCursor; public POINT ptScreenPos; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] private struct ICONINFO { public bool fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; } [DllImport("user32.dll")] private static extern bool GetCursorInfo(ref CURSORINFO pci); [DllImport("user32.dll")] private static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo); [DllImport("user32.dll")] private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int step, IntPtr brush, int flags); [DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr h); /// /// 영역을 캡처해 JPEG로. maxWidth보다 넓으면 줄임. /// hash: 변화 확인용 (같으면 보내지 않음) /// public static byte[] CaptureJpeg(Rectangle area, int maxWidth, long quality, out int width, out int height, out ulong hash) { using var bmp = new Bitmap(area.Width, area.Height, PixelFormat.Format32bppRgb); using (var g = Graphics.FromImage(bmp)) { g.CopyFromScreen(area.X, area.Y, 0, 0, area.Size, CopyPixelOperation.SourceCopy); DrawCursor(g, area); } hash = Hash(bmp); Bitmap output = bmp; Bitmap scaled = null; if (area.Width > maxWidth) { double r = (double)maxWidth / area.Width; int w = maxWidth, h = Math.Max(1, (int)(area.Height * r)); scaled = new Bitmap(w, h, PixelFormat.Format24bppRgb); using var g2 = Graphics.FromImage(scaled); g2.InterpolationMode = InterpolationMode.HighQualityBilinear; g2.DrawImage(bmp, 0, 0, w, h); output = scaled; } try { width = output.Width; height = output.Height; using var ms = new MemoryStream(); var codec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid); using var ep = new EncoderParameters(1); ep.Param[0] = new EncoderParameter(Encoder.Quality, quality); output.Save(ms, codec, ep); return ms.ToArray(); } finally { scaled?.Dispose(); } } private static void DrawCursor(Graphics g, Rectangle area) { try { var ci = new CURSORINFO { cbSize = Marshal.SizeOf() }; if (!GetCursorInfo(ref ci) || ci.flags != 1 /* CURSOR_SHOWING */) return; int hx = 0, hy = 0; if (GetIconInfo(ci.hCursor, out var ii)) { hx = ii.xHotspot; hy = ii.yHotspot; if (ii.hbmMask != IntPtr.Zero) DeleteObject(ii.hbmMask); if (ii.hbmColor != IntPtr.Zero) DeleteObject(ii.hbmColor); } int x = ci.ptScreenPos.X - area.X - hx; int y = ci.ptScreenPos.Y - area.Y - hy; if (x < -64 || y < -64 || x > area.Width || y > area.Height) return; var hdc = g.GetHdc(); try { DrawIconEx(hdc, x, y, ci.hCursor, 0, 0, 0, IntPtr.Zero, 3 /* DI_NORMAL */); } finally { g.ReleaseHdc(hdc); } } catch { } } /// 화면이 바뀌었는지 빠르게 확인하기 위한 값 (일부 픽셀만 사용) private static unsafe ulong Hash(Bitmap bmp) { var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb); try { ulong h = 1469598103934665603UL; byte* p = (byte*)data.Scan0; for (int y = 0; y < data.Height; y += 4) { uint* row = (uint*)(p + (long)y * data.Stride); for (int x = 0; x < data.Width; x += 4) { h ^= row[x]; h *= 1099511628211UL; } } return h; } finally { bmp.UnlockBits(data); } } } /// 대상 PC: 원격에서 받은 마우스/키보드 입력을 실행 public static class InputInjector { private const int INPUT_MOUSE = 0; private const int INPUT_KEYBOARD = 1; private const uint MOUSEEVENTF_MOVE = 0x0001, MOUSEEVENTF_LEFTDOWN = 0x0002, MOUSEEVENTF_LEFTUP = 0x0004, MOUSEEVENTF_RIGHTDOWN = 0x0008, MOUSEEVENTF_RIGHTUP = 0x0010, MOUSEEVENTF_MIDDLEDOWN = 0x0020, MOUSEEVENTF_MIDDLEUP = 0x0040, MOUSEEVENTF_WHEEL = 0x0800, MOUSEEVENTF_HWHEEL = 0x1000, MOUSEEVENTF_VIRTUALDESK = 0x4000, MOUSEEVENTF_ABSOLUTE = 0x8000; private const uint KEYEVENTF_EXTENDEDKEY = 0x0001, KEYEVENTF_KEYUP = 0x0002; [StructLayout(LayoutKind.Sequential)] private struct MOUSEINPUT { public int dx; public int dy; public int mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; } [StructLayout(LayoutKind.Explicit)] private struct InputUnion { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; } [StructLayout(LayoutKind.Sequential)] private struct INPUT { public int type; public InputUnion U; } [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); [DllImport("user32.dll")] private static extern uint MapVirtualKey(uint uCode, uint uMapType); /// /// 마우스. nx, ny = 캡처 영역 안의 비율(0~1). /// action: move / down / up / wheel / hwheel, button: left / right / middle /// public static void Mouse(Rectangle area, double nx, double ny, string action, string button, int delta) { var vs = Forms.SystemInformation.VirtualScreen; nx = Math.Clamp(nx, 0, 1); ny = Math.Clamp(ny, 0, 1); double px = area.X + nx * (area.Width - 1); double py = area.Y + ny * (area.Height - 1); int ax = (int)Math.Round((px - vs.X) * 65535.0 / Math.Max(1, vs.Width - 1)); int ay = (int)Math.Round((py - vs.Y) * 65535.0 / Math.Max(1, vs.Height - 1)); uint flags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK; int data = 0; switch (action) { case "down": flags |= button switch { "right" => MOUSEEVENTF_RIGHTDOWN, "middle" => MOUSEEVENTF_MIDDLEDOWN, _ => MOUSEEVENTF_LEFTDOWN }; break; case "up": flags |= button switch { "right" => MOUSEEVENTF_RIGHTUP, "middle" => MOUSEEVENTF_MIDDLEUP, _ => MOUSEEVENTF_LEFTUP }; break; case "wheel": flags |= MOUSEEVENTF_WHEEL; data = delta; break; case "hwheel": flags |= MOUSEEVENTF_HWHEEL; data = delta; break; } var input = new INPUT { type = INPUT_MOUSE }; input.U.mi = new MOUSEINPUT { dx = ax, dy = ay, mouseData = data, dwFlags = flags }; SendInput(1, new[] { input }, Marshal.SizeOf()); } /// 키보드 (가상 키 코드) public static void Key(int vk, bool down) { if (vk <= 0 || vk > 254) return; uint flags = down ? 0 : KEYEVENTF_KEYUP; if (IsExtended(vk)) flags |= KEYEVENTF_EXTENDEDKEY; var input = new INPUT { type = INPUT_KEYBOARD }; input.U.ki = new KEYBDINPUT { wVk = (ushort)vk, wScan = (ushort)MapVirtualKey((uint)vk, 0), dwFlags = flags }; SendInput(1, new[] { input }, Marshal.SizeOf()); } private static bool IsExtended(int vk) => vk switch { 0x21 or 0x22 or 0x23 or 0x24 => true, // PgUp PgDn End Home 0x25 or 0x26 or 0x27 or 0x28 => true, // 방향키 0x2D or 0x2E => true, // Insert Delete 0x5B or 0x5C or 0x5D => true, // Win Apps 0x6F => true, // Num / 0x90 => true, // NumLock 0xA3 or 0xA5 => true, // 오른쪽 Ctrl, Alt 0x2C => true, // PrintScreen _ => false }; } }