Home > Back-end >  Select proper fullscreen monitor output in WinUI 3
Select proper fullscreen monitor output in WinUI 3

Time:07-28

I am working on fullscreen application and I want to add funcitonality to select monitor which should be used to display applicaiton so you can swap from primary to other monitor. I checked some other applications which allow user to select display output for fullscreen or monitor for screen sharing. All of them use different naming for outputs.

Some examples:

  • Skype: All my monitors are called "Generic PnP Monitor"
  • Discord: Screen 1, Screen 2
  • World Of Warcraft: Primary, Monitor 1, Monitor 2
  • Advanced Display Settings (Windows): Display 1: Benq..., Display 2: T24i...

I already know that I can get DisplayArea using:

var areas = DisplayArea.FindAll();

Then I can iterate areas and get display name like this:

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

I know that DisplayArea has static getter for DisplayArea.Primary. those two things together might explain settings in WoW graphics where they put single primary by default and just rename MONITORINFOEX.DeviceName ot Monitor . Similar approach could be used in Discord.

BUT SKYPE...AND...WINDOWS SETTINGS...

I know that I can get "quite" nice display names like this (source):

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);
}

Best display names I get when I use this code:

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

But I can not get any information about WorkArea and Bounds in last two approaches. WorkArea and Bounds seems to be key to move my app window to correct screen.

Possible solution

I came with this idea: All three outputs seems to be sorted same way. Monitor 1 has always area 1, monitor 2 has area 2.

Question

Can I rely on this ordering behavior? Is there better approach to get display name, it's bounds and work area (also DPI would be great but I have a working function for this)?

External libraries:

PInvoke.User32

CodePudding user response:

WinRT classes (DisplayMonitor, etc.) are really what you want to use. The difficulty is they don't (AFAIK) expose the relation with the GDI device names (\\?\DISPLAY1, etc.).

But their implementation is based on the native Connecting and configuring displays API and this API can give you the GDI name using a code like this:

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);

Now, adapterId is easy to find, but the sourceId is more complex. It doesn't seem to be exposed directly by WinRT, you must use the IDisplayPathInterop Interface, so:

[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);
}

Now, you can dump all monitors with their GDI names, and then you can use that GDI name to correlate using other Windows API : MONITOR, DISPLAY_DEVICE, event the old Winform's Screen, etc. like you do with a code like this:

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
            }
        }
    }
}

Which will display this on my system (note display 2 is my primary screen - with sourceId 1, not 0 -, this demonstrates you cannot just use indices to match):

C27HG7x on \\.\DISPLAY2
DELL U2715H on \\.\DISPLAY1
  • Related