feat: add primary display feature

This commit is contained in:
Nguyen Duy
2026-05-14 17:14:07 +07:00
parent 95a20eee3d
commit 9b625a5bf5
6 changed files with 74 additions and 0 deletions
+2
View File
@@ -26,6 +26,7 @@
</Border.Style>
<Grid>
<TextBlock x:Name="xDeg" d:Text="90°" Foreground="White" Margin="8,4,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TextBlock x:Name="xPri" d:Text="[P]" Foreground="White" Margin="0,4,8,0" HorizontalAlignment="Right" VerticalAlignment="Top" />
<TextBlock Text="{Binding DisplayNum}" d:Text="1" FontSize="24" FontWeight="SemiBold" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,5" />
<TextBlock Grid.Row="1" Text="{Binding DisplayMode}" d:Text="1920 x 1080 @ 60 Hz" FontSize="12" Foreground="White" TextAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,8" />
</Grid>
@@ -52,6 +53,7 @@
<Separator />
<MenuItem Click="TakeScreenshot" Header="{DynamicResource t_take_screenshot}" IsEnabled="{Binding Active}" />
<MenuItem Click="MirrorScreen" Header="{DynamicResource t_mirror_screen}" IsEnabled="{Binding Active}" />
<MenuItem Click="SetAsPrimary" Header="{DynamicResource t_set_primary}" IsEnabled="{Binding CanSetPrimary}" />
<Separator />
<MenuItem Click="RemoveDisplay" Header="{DynamicResource t_remove}" />
</ContextMenu>
+11
View File
@@ -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();
}
}
}
+58
View File
@@ -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;
}
/// <summary>
/// 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.
/// </summary>
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)]
+1
View File
@@ -23,6 +23,7 @@
<system:String x:Key="t_portrait_flipped">Portrait (flipped)</system:String>
<system:String x:Key="t_take_screenshot">Take screenshot</system:String>
<system:String x:Key="t_mirror_screen">Mirror screen</system:String>
<system:String x:Key="t_set_primary">Set as primary</system:String>
<system:String x:Key="t_remove">Remove</system:String>
<!--Tray menu-->
+1
View File
@@ -23,6 +23,7 @@
<system:String x:Key="t_portrait_flipped">Dọc (lật ngược)</system:String>
<system:String x:Key="t_take_screenshot">Chụp màn hình</system:String>
<system:String x:Key="t_mirror_screen">Phản chiếu màn hình</system:String>
<system:String x:Key="t_set_primary">Đặt làm màn hình chính</system:String>
<system:String x:Key="t_remove">Tháo bỏ</system:String>
<!--Tray menu-->
+1
View File
@@ -23,6 +23,7 @@
<system:String x:Key="t_portrait_flipped">纵向 (翻转)</system:String>
<system:String x:Key="t_take_screenshot">屏幕截图</system:String>
<system:String x:Key="t_mirror_screen">镜像屏幕</system:String>
<system:String x:Key="t_set_primary">设为主显示器</system:String>
<system:String x:Key="t_remove">移除</system:String>
<!--Tray menu-->