在WinUI 3中选择合适的全屏监视器输出



我正在开发全屏应用程序,我想添加功能来选择应该用于显示应用程序的监视器,这样您就可以从主监视器切换到其他监视器。我检查了一些其他应用程序,这些应用程序允许用户选择全屏显示输出或屏幕共享监视器。所有这些都对输出使用不同的命名。

一些例子:

  • Skype:我所有的显示器都被称为"通用PnP监视器">
  • Discord:屏幕1,屏幕2
  • 魔兽世界:初级,监视器1,监视器2
  • 高级显示设置(Windows(:显示1:Benq。。。,显示器2:T24i

我已经知道我可以使用获得DisplayArea

var areas = DisplayArea.FindAll();

然后我可以迭代areas,得到这样的显示名称:

var area0 = areas[0];
var monitorHw = Microsoft.UI.Win32Interop.GetMonitorFromDisplayId(area0.DisplayId);
PInvoke.User32.GetMonitorInfo(monitorHw, out var monitorInfo1);
var monitorName1 = new string(monitorInfo1.DeviceName); // monitorName1 = \.DISPLAY1

我知道DisplayAreaDisplayArea.Primary有静态getter。这两件事加在一起可能解释了魔兽世界图形中的设置,默认情况下,他们只放置一个主,并将MONITORINFOEX.DeviceName重命名为监视器。类似的方法也可以用于Discord。

但是SKYPE。。。而且。。。窗口设置。。。

我知道我可以得到";相当";漂亮的显示名称如下(来源(:

public static unsafe void GetDisplayNames()
{
DISPLAY_DEVICE lpDisplayDevice = new();
lpDisplayDevice.cb = (uint) Marshal.SizeOf(lpDisplayDevice);
DISPLAY_DEVICE monitor_name = new();
monitor_name.cb = (uint) Marshal.SizeOf(monitor_name);
uint devNum = 0;
var msg = "";
while (PInvoke.User32.EnumDisplayDevices(null, devNum, ref lpDisplayDevice, 0))
{
msg += "DeviceName =" + new string(lpDisplayDevice.DeviceName).Trim() + 'n';
PInvoke.User32.EnumDisplayDevices(new string(lpDisplayDevice.DeviceName), 0, ref monitor_name, 0);
msg += "Monitor name =" + new string(monitor_name.DeviceString).Trim() + 'n';
++devNum;
}
Console.WriteLine(msg);
}

当我使用这个代码时,我得到的最佳显示名称:

var displayMonitorSelector = DisplayMonitor.GetDeviceSelector();
var displayMonitorDeviceInformation = (await DeviceInformation.FindAllAsync(displayMonitorSelector))[0];
DisplayMonitor? displayMonitor = await DisplayMonitor.FromInterfaceIdAsync(displayMonitorDeviceInformation.Id);
var displayMonitorName = displayMonitor.DisplayName; // BenQ EX3203R
// OR (Not sure which works better for me yet)
var projectionSelector = ProjectionManager.GetDeviceSelector();
var projectionDeviceInformation = (await DeviceInformation.FindAllAsync(projectionSelector))[0];
DisplayMonitor? projection = await DisplayMonitor.FromInterfaceIdAsync(projectionDeviceInformation.Id);
var projectionName = projection.DisplayName; // BenQ EX3203R

但在最后两种方法中,我没有得到关于WorkAreaBounds的任何信息。WorkAreaBounds似乎是将我的应用程序窗口移动到正确屏幕的关键。

可能的解决方案

我提出了这样的想法:所有三个输出似乎都以相同的方式排序。监视器1始终具有区域1,监视器2具有区域2。

问题

我可以依赖这种订购行为吗?有没有更好的方法来获取显示名称、边界和工作区域(DPI也很棒,但我有一个工作功能(?

外部库:

PInvoke.User32

WinRT类(DisplayMonitor等(才是您真正想要使用的。困难在于它们不(AFAIK(公开与GDI设备名称的关系(\\?\DISPLAY1等(。

但是它们的实现是基于原生的连接和配置显示API,这个API可以使用如下代码为您提供GDI名称:

public static string GetGdiDeviceName(int adapterIdHigh, uint adapterIdLow, uint sourceId)
{
var info = new DISPLAYCONFIG_SOURCE_DEVICE_NAME();
const int DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME = 1;
info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
info.header.size = Marshal.SizeOf<DISPLAYCONFIG_SOURCE_DEVICE_NAME>();
info.header.adapterIdHigh = adapterIdHigh;
info.header.adapterIdLow = adapterIdLow;
info.header.id = sourceId;
var err = DisplayConfigGetDeviceInfo(ref info);
if (err != 0)
throw new Win32Exception(err);
return info.viewGdiDeviceName;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DISPLAYCONFIG_SOURCE_DEVICE_NAME
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER header;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string viewGdiDeviceName;
public override string ToString() => viewGdiDeviceName;
}
[StructLayout(LayoutKind.Sequential)]
private struct DISPLAYCONFIG_DEVICE_INFO_HEADER
{
public int type;
public int size;
public uint adapterIdLow;
public int adapterIdHigh;
public uint id;
}
[DllImport("user32")]
private static extern int DisplayConfigGetDeviceInfo(ref DISPLAYCONFIG_SOURCE_DEVICE_NAME requestPacket);

现在,adapterId很容易找到,但sourceId更复杂。它似乎不是由WinRT直接公开的,您必须使用IDisplayPathInterop接口,所以:

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("A6BA4205-E59E-4E71-B25B-4E436D21EE3D")]
private interface IDisplayPathInterop
{
[PreserveSig]
int CreateSourcePresentationHandle(out IntPtr value);
[PreserveSig]
int GetSourceId(out uint sourceId);
}

现在,您可以使用GDI名称转储所有监视器,然后您可以使用该GDI名称使用其他Windows API进行关联:MONITOR、DISPLAY_DEVICE、event the old Winform’s Screen等,就像您使用以下代码所做的那样:

using (var mgr = DisplayManager.Create(DisplayManagerOptions.None))
{
var state = mgr.TryReadCurrentStateForAllTargets().State;
foreach (var view in state.Views)
{
foreach (var path in view.Paths)
{
var monitor = path.Target.TryGetMonitor();
if (monitor != null)
{
var ip = WinRT.CastExtensions.As<IDisplayPathInterop>(path);
ip.GetSourceId(out var sourceId);
var gdiDeviceName = GetGdiDeviceName(monitor.DisplayAdapterId.HighPart, monitor.DisplayAdapterId.LowPart, sourceId);
Console.WriteLine(monitor.DisplayName + " on " + gdiDeviceName);
// TODO: use gdiDeviceName to correlate with other APIs
}
}
}
}

它将在我的系统上显示这一点(注意,显示2是我的主屏幕,源ID为1,而不是0,这表明你不能只使用索引来匹配(:

C27HG7x on \.DISPLAY2
DELL U2715H on \.DISPLAY1

最新更新