Files
parsec-vdd/app/MirrorWindow.cs
T

441 lines
16 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;
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 ParsecDisplay
{
public class MirrorWindow : Form
{
private bool IsMirroring;
private Thread MirrorThread;
private TaskCompletionSource<IntPtr> WhenHwnd;
public MirrorWindow()
{
IsMirroring = false;
WhenHwnd = new TaskCompletionSource<IntPtr>();
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<Native.DEVMODE>();
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<Native.CURSORINFO>();
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<Native.BITMAP>(), ref bmpCursor);
}
else
{
Native.GetObject(iconInfo.hbmMask, Marshal.SizeOf<Native.BITMAP>(), 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);
}
}
}