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
};
}
}