From 9b625a5bf59ecea0aa1beffc494b12cf984f6c38 Mon Sep 17 00:00:00 2001 From: Nguyen Duy Date: Thu, 14 May 2026 17:14:07 +0700 Subject: [PATCH] feat: add primary display feature --- app/Components/DisplayItem.xaml | 2 ++ app/Components/DisplayItem.xaml.cs | 11 ++++++ app/Display.cs | 58 ++++++++++++++++++++++++++++++ app/Languages/en.xaml | 1 + app/Languages/vi.xaml | 1 + app/Languages/zh.xaml | 1 + 6 files changed, 74 insertions(+) diff --git a/app/Components/DisplayItem.xaml b/app/Components/DisplayItem.xaml index d5d6f37..da4a17d 100644 --- a/app/Components/DisplayItem.xaml +++ b/app/Components/DisplayItem.xaml @@ -26,6 +26,7 @@ + @@ -52,6 +53,7 @@ + diff --git a/app/Components/DisplayItem.xaml.cs b/app/Components/DisplayItem.xaml.cs index c937dd3..ab72c47 100644 --- a/app/Components/DisplayItem.xaml.cs +++ b/app/Components/DisplayItem.xaml.cs @@ -11,6 +11,7 @@ namespace ParsecVDisplay.Components public partial class DisplayItem : UserControl { public bool Active { get; set; } = true; + public bool CanSetPrimary { get; set; } = true; public string DisplayNum { get; set; } = "1"; public string DisplayName { get; set; } = "Display [1]"; public string DisplayPath { get; set; } = "\\\\.\\DISPLAY1"; @@ -31,6 +32,7 @@ namespace ParsecVDisplay.Components { Display = display; Active = display.Active; + CanSetPrimary = display.Active && !display.Primary; DisplayNum = $"{display.Identifier}"; DisplayName = $"Display [{display.Identifier}]"; @@ -40,6 +42,9 @@ namespace ParsecVDisplay.Components ? Visibility.Hidden : Visibility.Visible; xDeg.Text = string.Format("{0}°", ((int)display.CurrentOrientation * 90)); + xPri.Visibility = display.Primary ? Visibility.Visible : Visibility.Hidden; + xPri.Text = "[P]"; + if (display.Active) { DisplayMode = display.CurrentMode.ToString(); @@ -236,5 +241,11 @@ namespace ParsecVDisplay.Components { Tray.Instance.Invoke(() => Tray.Instance.RemoveDisplay(Display.DisplayIndex)); } + + private void SetAsPrimary(object sender, RoutedEventArgs e) + { + if (Active && !Display.Primary) + Display.SetPrimary(); + } } } \ No newline at end of file diff --git a/app/Display.cs b/app/Display.cs index 885f3cd..2f398e1 100644 --- a/app/Display.cs +++ b/app/Display.cs @@ -123,6 +123,7 @@ namespace ParsecVDisplay } public bool Active; + public bool Primary; public int Identifier; public int CloneOf; public int Address; @@ -289,6 +290,60 @@ namespace ParsecVDisplay return Native.ChangeDisplaySettingsEx(null, IntPtr.Zero, IntPtr.Zero, 0, IntPtr.Zero) == 0; } + /// + /// Make this display the primary monitor. The desktop origin must land + /// on the new primary, so we shift every active display by this one's + /// current position before flagging it CDS_SET_PRIMARY, then commit + /// all changes atomically. Matches the vdd.c reference implementation. + /// + public bool SetPrimary() + { + if (string.IsNullOrEmpty(DeviceName)) + return false; + + // Capture our current position — every other display will be shifted + // by this offset so we end up at (0,0). + var target = new Native.DEVMODE(); + target.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE)); + if (!Native.EnumDisplaySettings(DeviceName, -1, ref target)) + return false; + + int offX = target.dmPositionX; + int offY = target.dmPositionY; + + var dd = new Native.DISPLAY_DEVICE(); + dd.cb = Marshal.SizeOf(typeof(Native.DISPLAY_DEVICE)); + + bool targetOk = false; + for (int i = 0; Native.EnumDisplayDevices(null, i, ref dd, 0); i++) + { + if ((dd.StateFlags & Native.DISPLAY_DEVICE_ACTIVE) == 0) + continue; + + var dm = new Native.DEVMODE(); + dm.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE)); + if (!Native.EnumDisplaySettings(dd.DeviceName, -1, ref dm)) + continue; + + dm.dmPositionX -= offX; + dm.dmPositionY -= offY; + dm.dmFields = /*DM_POSITION*/ 0x20; + + uint flags = /*CDS_UPDATEREGISTRY*/ 0x1 | /*CDS_NORESET*/ 0x10000000; + bool isTarget = string.Equals(dd.DeviceName, DeviceName, StringComparison.Ordinal); + if (isTarget) + flags |= /*CDS_SET_PRIMARY*/ 0x10; + + int rc = Native.ChangeDisplaySettingsEx(dd.DeviceName, ref dm, IntPtr.Zero, flags, IntPtr.Zero); + if (isTarget && rc == 0) + targetOk = true; + } + + // Commit pending positional changes + the primary flag together. + CommitChanges(); + return targetOk; + } + public void TakeScreenshot(string saveFile) { if (string.IsNullOrEmpty(DeviceName)) @@ -367,6 +422,8 @@ namespace ParsecVDisplay var display = new Display { Active = (dd2.StateFlags & Native.DISPLAY_DEVICE_ACTIVE) != 0, + // Primary is a per-adapter flag on the outer dd, not on the monitor + Primary = (dd.StateFlags & Native.DISPLAY_DEVICE_PRIMARY_DEVICE) != 0, Address = ParseDisplayAddress(dd2.DeviceID), DeviceName = dd.DeviceName, DisplayName = ParseDisplayCode(dd2.DeviceID), @@ -507,6 +564,7 @@ namespace ParsecVDisplay public const uint EDD_GET_DEVICE_INTERFACE_NAME = 0x1; public const uint DISPLAY_DEVICE_ACTIVE = 0x1; public const uint DISPLAY_DEVICE_ATTACHED = 0x2; + public const uint DISPLAY_DEVICE_PRIMARY_DEVICE = 0x4; [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] diff --git a/app/Languages/en.xaml b/app/Languages/en.xaml index 33471f6..c746f11 100644 --- a/app/Languages/en.xaml +++ b/app/Languages/en.xaml @@ -23,6 +23,7 @@ Portrait (flipped) Take screenshot Mirror screen + Set as primary Remove diff --git a/app/Languages/vi.xaml b/app/Languages/vi.xaml index c8c2828..73af80a 100644 --- a/app/Languages/vi.xaml +++ b/app/Languages/vi.xaml @@ -23,6 +23,7 @@ Dọc (lật ngược) Chụp màn hình Phản chiếu màn hình + Đặt làm màn hình chính Tháo bỏ diff --git a/app/Languages/zh.xaml b/app/Languages/zh.xaml index 7bca70c..f1af4e4 100644 --- a/app/Languages/zh.xaml +++ b/app/Languages/zh.xaml @@ -23,6 +23,7 @@ 纵向 (翻转) 屏幕截图 镜像屏幕 + 设为主显示器 移除