diff --git a/app/Components/DisplayItem.xaml b/app/Components/DisplayItem.xaml
index 78b98c5..8b765a6 100644
--- a/app/Components/DisplayItem.xaml
+++ b/app/Components/DisplayItem.xaml
@@ -55,7 +55,9 @@
+
+
diff --git a/app/Components/DisplayItem.xaml.cs b/app/Components/DisplayItem.xaml.cs
index be6a8d9..290658f 100644
--- a/app/Components/DisplayItem.xaml.cs
+++ b/app/Components/DisplayItem.xaml.cs
@@ -221,6 +221,13 @@ namespace ParsecVDisplay.Components
});
}
+ private void MirrorScreen(object sender, RoutedEventArgs e)
+ {
+ var mirrorWindow = new MirrorWindow();
+ mirrorWindow.MirrorScreen(Display.DeviceName);
+ mirrorWindow.Show();
+ }
+
private void RemoveDisplay(object sender, RoutedEventArgs e)
{
Tray.Instance.Invoke(() => Tray.Instance.RemoveDisplay(Display.DisplayIndex));
diff --git a/app/Config.cs b/app/Config.cs
index be3c76e..1acbe9e 100644
--- a/app/Config.cs
+++ b/app/Config.cs
@@ -48,6 +48,12 @@ namespace ParsecVDisplay
set => SetInt(nameof(SkipDriverCheck), value ? 1 : 0);
}
+ public static int MirroringFPS
+ {
+ get => GetInt(nameof(MirroringFPS), 30);
+ set => SetInt(nameof(MirroringFPS), value);
+ }
+
#region Registry data store
static string GetString(string key, string @default)
diff --git a/app/MirrorWindow.cs b/app/MirrorWindow.cs
new file mode 100644
index 0000000..f65909d
--- /dev/null
+++ b/app/MirrorWindow.cs
@@ -0,0 +1,406 @@
+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 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)
+ {
+ 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)
+ {
+ // 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();
+
+ 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(), 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);
+ }
+ }
+}
\ No newline at end of file