using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace ParsecVDisplay { public class MirrorWindow : Form { private bool IsMirroring; private Thread MirrorThread; private TaskCompletionSource WhenHwnd; public MirrorWindow() { IsMirroring = false; WhenHwnd = new TaskCompletionSource(); ClientSize = new Size(960, 540); Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } protected override void OnFormClosing(FormClosingEventArgs e) { IsMirroring = false; MirrorThread?.Join(); base.OnFormClosing(e); } protected override void OnPaint(PaintEventArgs e) { } protected override void OnPaintBackground(PaintEventArgs e) { } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); WhenHwnd.SetResult(Handle); } public void MirrorScreen(string displayDevice) { // Phantom (PnP-only) displays have no GDI handle; nothing to mirror. if (string.IsNullOrEmpty(displayDevice)) return; if (!IsMirroring) { IsMirroring = true; Text = $"Mirror - {displayDevice}"; int fps = Config.MirroringFPS; MirrorThread = new Thread(() => MirrorWorker(displayDevice, fps)); MirrorThread.IsBackground = true; MirrorThread.Start(); } } private void MirrorWorker(string displayDevice, int fps) { var hwnd = WhenHwnd.Task.Result; var dcDest = Native.GetDC(hwnd); var bgBrush = Native.GetStockObject(/*BLACK_BRUSH*/ 4); var devmode = default(Native.DEVMODE); short devmodeSize = (short)Marshal.SizeOf(); try { var stopwatch = Stopwatch.StartNew(); double previousTime = stopwatch.Elapsed.TotalMilliseconds; double frameTime = 1000.0 / fps; while (IsMirroring) { double currentTime = stopwatch.Elapsed.TotalMilliseconds; double elapsedTime = currentTime - previousTime; if (elapsedTime >= frameTime) { devmode.dmSize = devmodeSize; if (Native.EnumDisplaySettings(displayDevice, -1, ref devmode)) { var dcScreens = Native.GetDC(IntPtr.Zero); var client = GetClientSize(hwnd); var screen = new Rectangle(devmode.dmPositionX, devmode.dmPositionY, devmode.dmPelsWidth, devmode.dmPelsHeight); var vp = GetViewport(client.Width, client.Height, screen.Width, screen.Height); DrawBackground(dcDest, bgBrush, ref client, ref vp); DrawScreen(dcDest, dcScreens, ref vp, ref screen); DrawCursor(dcDest, ref vp, ref screen); Native.ReleaseDC(IntPtr.Zero, dcScreens); } previousTime = currentTime; } else { int sleepTime = (int)(frameTime - elapsedTime); if (sleepTime > 0) Thread.Sleep(sleepTime); } } } finally { Native.ReleaseDC(hwnd, dcDest); Native.DeleteObject(bgBrush); } } private struct Viewport { public int X; public int Y; public int Width; public int Height; } private static void DrawBackground(IntPtr dc, IntPtr brush, ref Size client, ref Viewport vp) { var rect = default(Rectangle); // fill the excluded rectangles (areas outside the viewport) // this is the simplest way to avoid flickering without WM_PAINT // top excluded rect if (vp.Y > 0) { rect.X = 0; rect.Y = 0; rect.Width = client.Width; rect.Height = vp.Y; Native.FillRect(dc, ref rect, brush); } // bottom excluded rect if (vp.Y + vp.Height < client.Height) { rect.X = 0; rect.Y = vp.Y + vp.Height; rect.Width = client.Width; rect.Height = client.Height; Native.FillRect(dc, ref rect, brush); } // left excluded rect if (vp.X > 0) { rect.X = 0; rect.Y = vp.Y; rect.Width = vp.X; rect.Height = vp.Height + vp.Y; Native.FillRect(dc, ref rect, brush); } // right excluded rect if (vp.X + vp.Width < client.Width) { rect.X = vp.X + vp.Width; rect.Y = vp.Y; rect.Width = client.Width; rect.Height = vp.Height + vp.Y; Native.FillRect(dc, ref rect, brush); } } private static void DrawScreen(IntPtr dc, IntPtr dcSrc, ref Viewport vp, ref Rectangle screen) { // HALFTONE gives the cleanest scaled output; MSDN requires calling // SetBrushOrgEx immediately after to avoid a faint color/dither // shift in the result. Native.SetStretchBltMode(dc, /*HALFTONE*/ 4); Native.SetBrushOrgEx(dc, 0, 0, IntPtr.Zero); // SRCCOPY | CAPTUREBLT — CAPTUREBLT pulls in layered / DWM-composited // windows (Discord, Teams, some game overlays) that plain SRCCOPY skips. Native.StretchBlt( dc, vp.X, vp.Y, vp.Width, vp.Height, dcSrc, screen.X, screen.Y, screen.Width, screen.Height, Native.SRCCOPY | Native.CAPTUREBLT ); } private static void DrawCursor(IntPtr dc, ref Viewport vp, ref Rectangle screen) { var cursor = default(Native.CURSORINFO); cursor.cbSize = Marshal.SizeOf(); if (!Native.GetCursorInfo(ref cursor) || !screen.Contains(cursor.screenPosX, cursor.screenPosY) || cursor.flags != /*CURSOR_SHOWING*/ 0x1) return; var iconInfo = default(Native.ICONINFO); if (!Native.GetIconInfo(cursor.hCursor, ref iconInfo)) return; try { var bmpCursor = default(Native.BITMAP); // Monochrome (mask-only) cursors — e.g. the text I-beam — have a // NULL hbmColor. Their geometry lives in hbmMask, whose height is // 2× the displayed height (top half = AND mask, bottom = XOR mask). if (iconInfo.hbmColor != IntPtr.Zero) { Native.GetObject(iconInfo.hbmColor, Marshal.SizeOf(), ref bmpCursor); } else { Native.GetObject(iconInfo.hbmMask, Marshal.SizeOf(), ref bmpCursor); bmpCursor.bmHeight /= 2; } int x = cursor.screenPosX - iconInfo.xHotspot - screen.X; int y = cursor.screenPosY - iconInfo.yHotspot - screen.Y; int width = bmpCursor.bmWidth; int height = bmpCursor.bmHeight; ScaleCursor(ref vp, screen.Size, ref x, ref y, ref width, ref height); Native.DrawIconEx(dc, x, y, cursor.hCursor, width, height, 0, IntPtr.Zero, /*DI_NORMAL*/ 0x3); } finally { // GetIconInfo creates these bitmaps and the caller MUST delete // them — leaking them here (60×/sec at 30 fps) eventually // exhausts the process's GDI handle quota. if (iconInfo.hbmMask != IntPtr.Zero) Native.DeleteObject(iconInfo.hbmMask); if (iconInfo.hbmColor != IntPtr.Zero) Native.DeleteObject(iconInfo.hbmColor); } } private static Size GetClientSize(IntPtr hwnd) { var rect = new Rectangle(); Native.GetClientRect(hwnd, ref rect); return new Size(rect.Width, rect.Height); } private static Viewport GetViewport(int clientWidth, int clientHeight, int rectWidth, int rectHeight) { float clientAspect = (float)clientWidth / clientHeight; float rectAspect = (float)rectWidth / rectHeight; int viewportX, viewportY; int viewportWidth, viewportHeight; // compare aspect ratios to determine scaling if (clientAspect > rectAspect) { // client is wider than rect, so scale to fit height viewportHeight = clientHeight; viewportWidth = (int)(rectAspect * viewportHeight); } else { // client is taller than rect, so scale to fit width viewportWidth = clientWidth; viewportHeight = (int)(viewportWidth / rectAspect); } // center the viewport viewportX = (clientWidth - viewportWidth) / 2; viewportY = (clientHeight - viewportHeight) / 2; return new Viewport { X = viewportX, Y = viewportY, Width = viewportWidth, Height = viewportHeight, }; } private static void ScaleCursor(ref Viewport viewport, Size screen, ref int cursorX, ref int cursorY, ref int cursorWidth, ref int cursorHeight) { float scaleX = (float)viewport.Width / screen.Width; float scaleY = (float)viewport.Height / screen.Height; cursorWidth = (int)(cursorWidth * scaleX); cursorHeight = (int)(cursorHeight * scaleY); cursorX = viewport.X + (int)(cursorX * scaleX); cursorY = viewport.Y + (int)(cursorY * scaleY); } private static class Native { public const int ENUM_CURRENT_SETTINGS = -1; public const uint SRCCOPY = 0x00CC0020; public const uint CAPTUREBLT = 0x40000000; [StructLayout(LayoutKind.Sequential)] public struct DEVMODE { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName; public short dmSpecVersion; public short dmDriverVersion; public short dmSize; public short dmDriverExtra; public int dmFields; public int dmPositionX; public int dmPositionY; public int dmDisplayOrientation; public int dmDisplayFixedOutput; public short dmColor; public short dmDuplex; public short dmYResolution; public short dmTTOption; public short dmCollate; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName; public short dmLogPixels; public int dmBitsPerPel; public int dmPelsWidth; public int dmPelsHeight; public int dmDisplayFlags; public int dmDisplayFrequency; public int dmICMMethod; public int dmICMIntent; public int dmMediaType; public int dmDitherType; public int dmReserved1; public int dmReserved2; public int dmPanningWidth; public int dmPanningHeight; } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetClientRect(IntPtr hwnd, ref Rectangle rect); [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hwnd); [DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); [DllImport("gdi32.dll", SetLastError = true)] public static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData); [DllImport("gdi32.dll", SetLastError = true)] public static extern bool DeleteDC(IntPtr hdc); [DllImport("gdi32.dll", SetLastError = true)] public static extern bool DeleteObject(IntPtr hObject); [DllImport("gdi32.dll")] public static extern int SetStretchBltMode(IntPtr hdc, int mode); [DllImport("gdi32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetBrushOrgEx(IntPtr hdc, int x, int y, IntPtr lppt); [DllImport("gdi32.dll")] public static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, uint dwRop); [StructLayout(LayoutKind.Sequential)] public struct BITMAP { public uint bmType; public int bmWidth; public int bmHeight; public int bmWidthBytes; public short bmPlanes; public short bmBitsPixel; public IntPtr bmBits; } [StructLayout(LayoutKind.Sequential)] public struct ICONINFO { public int fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; } [StructLayout(LayoutKind.Sequential)] public struct CURSORINFO { public int cbSize; public uint flags; public IntPtr hCursor; public int screenPosX; public int screenPosY; } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetIconInfo(IntPtr hIcon, ref ICONINFO piconinfo); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetCursorInfo(ref CURSORINFO pci); [DllImport("gdi32.dll")] public static extern int GetObject(IntPtr h, int c, ref BITMAP pv); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyWidth, uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags); [DllImport("user32.dll")] public static extern int FillRect(IntPtr hDC, ref Rectangle lprc, IntPtr hbr); [DllImport("gdi32.dll")] public static extern IntPtr GetStockObject(int i); } } }