Files
parsec-vdd/app/MirrorWindow.cs
T
Nguyen Duy 61e129cf6d feat: add device enumeration for PnP displays (#82)
- Legacy EnumDisplayDevices is bound to the calling thread, it's not working on RDP session.
2026-05-14 15:58:13 +07:00

410 lines
14 KiB
C#

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<IntPtr> WhenHwnd;
public MirrorWindow()
{
IsMirroring = false;
WhenHwnd = new TaskCompletionSource<IntPtr>();
ClientSize = new Size(960, 540);
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
protected override void OnClosing(CancelEventArgs e)
{
IsMirroring = false;
MirrorThread?.Join();
base.OnClosing(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)
{
// set scaling mode
Native.SetStretchBltMode(dc, /*HALFTONE*/ 4);
// draw the screen
Native.StretchBlt(
dc,
vp.X, vp.Y, vp.Width, vp.Height,
dcSrc,
screen.X, screen.Y, screen.Width, screen.Height,
Native.SRCCOPY
);
}
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)
// cursor must be inside the screen
&& screen.Contains(cursor.screenPosX, cursor.screenPosY)
// and visible
&& cursor.flags == /*CURSOR_SHOWING*/ 0x1)
{
var iconInfo = default(Native.ICONINFO);
Native.GetIconInfo(cursor.hCursor, ref iconInfo);
var bmpCursor = default(Native.BITMAP);
Native.GetObject(iconInfo.hbmColor, Marshal.SizeOf<Native.BITMAP>(), ref bmpCursor);
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);
}
}
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 int SRCCOPY = 0x00CC0020;
[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")]
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);
}
}
}