283 lines
11 KiB
C#
283 lines
11 KiB
C#
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
|
|
{
|
|
/// <summary>모니터 정보 (실제 픽셀 좌표)</summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>대상 PC: 화면 캡처</summary>
|
|
public static class ScreenCapture
|
|
{
|
|
/// <summary>모니터 목록 (1부터, 주 모니터 우선이 아니라 Windows 순서)</summary>
|
|
public static List<MonitorInfo> Monitors()
|
|
{
|
|
var list = new List<MonitorInfo>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>monitor 0 = 전체 화면, 1~ = 해당 모니터</summary>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// 영역을 캡처해 JPEG로. maxWidth보다 넓으면 줄임.
|
|
/// hash: 변화 확인용 (같으면 보내지 않음)
|
|
/// </summary>
|
|
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<CURSORINFO>() };
|
|
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 { }
|
|
}
|
|
|
|
/// <summary>화면이 바뀌었는지 빠르게 확인하기 위한 값 (일부 픽셀만 사용)</summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>대상 PC: 원격에서 받은 마우스/키보드 입력을 실행</summary>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// 마우스. nx, ny = 캡처 영역 안의 비율(0~1).
|
|
/// action: move / down / up / wheel / hwheel, button: left / right / middle
|
|
/// </summary>
|
|
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<INPUT>());
|
|
}
|
|
|
|
/// <summary>키보드 (가상 키 코드)</summary>
|
|
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<INPUT>());
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|