Search code examples
c#windows.net-corescreen-capture

How do I get a `DisplayId` for `GraphicsCaptureItem.TryCreateFromDisplayId` in C#?


I'm trying to do screen capture in my app, where I capture the current display. I've been using GraphicsCaptureItem.TryCreateFromDisplayId(Windows.Graphics.DisplayId) with a dummy new DisplayId(0), but that's capturing a point in between both of my monitors. Unfortunately, the DisplayId class does not indicate how I might create one with the correct value.

How do I create a Windows.Graphics.DisplayId properly? Or how might I get a Windows.UI.WindowId, for .TryCreateFromWindowId instead?


Solution

  • Option 1: Convert to DisplayIds

    The DisplayId class seems to be a simple wrapper around an HMONITOR, so you can obtain an HMONITOR however you'd like and convert it into a DisplayId (WindowIds seem to be wrappers around HWNDs, too). For example, using CsWin32 (or manual PInvoke) to use the MonitorFromWindow APIs:

    // Get the display for an arbitrary HWND
    internal static DisplayId GetDisplayIdForHwnd(HWND hwnd)
    {
        Windows.Win32.Graphics.Gdi.HMONITOR monitor = Windows.Win32.PInvoke.MonitorFromWindow(hwnd, Windows.Win32.Graphics.Gdi.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
        var displayId = new DisplayId((ulong)monitor.Value);
        return displayId;
    }
    
    // Get the default display by harnessing `MONITOR_DEFAULTTOPRIMARY`
    public static DisplayId GetDefaultDisplayId()
    {
        return GetDisplayIdForHwnd(HWND.Null);
    }
    

    And then something like:

    var displayId = GetDisplayIdForHwnd(hwnd);
    var captureItem = GraphicsCaptureItem.TryCreateFromDisplayId(displayId);
    

    You could also use an API like EnumDisplayMonitors to enumerate HMONITORs.

    Option 2: Use IGraphicsCaptureItemInterop

    As of 2019, Windows provides the IGraphicsCaptureItemInterop interface as compat, which notably offers IGraphicsCaptureItemInterop::CreateForMonitor(HMONITOR) and IGraphicsCaptureItemInterop::CreateForWindow(HWND), which this small sample app explains.

    As this other question alludes, you can use this interface from C# if you write the correct PInvoke script (rewritten here with more-modern marshalling):

    public static partial class WindowCapture
    {
        private static Guid GraphicsCaptureItemGuid = new ("79C3F95B-31F7-4EC2-A464-632EF5D30760");
    
        [ComImport]
        [Guid("3628E81B-3CAC-4C60-B7F4-23CE0E0C3356")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [ComVisible(true)]
        private interface IGraphicsCaptureItemInterop
        {
            IntPtr CreateForWindow(
                [In] IntPtr window,
                [In] ref Guid iid);
    
            IntPtr CreateForMonitor(
                [In] IntPtr monitor,
                [In] ref Guid iid);
        }
    
        // Obtain the HMONITOR as before, like:
        // var monitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
        private static GraphicsCaptureItem CreateForMonitor(HMONITOR hmon)
        {
            var interop = GraphicsCaptureItem.As<IGraphicsCaptureItemInterop>();
            var ptr = interop.CreateForMonitor(new IntPtr(hmon.Value), GraphicsCaptureItemGuid);
            var captureItem = GraphicsCaptureItem.FromAbi(ptr);
            return captureItem;
        }
    }
    

    Option 3: Use the WinAppSDK Interop APIs

    If you're using WinAppSDK, there is an official API (Win32Interop.GetDisplayIdFromMonitor(IntPtr) and vice versa). However, the Interop API actually returns a Microsoft.UI.DisplayId, which you'd need to convert into a Windows.Graphics.DisplayId manually anyway:

    Windows.Graphics.DisplayId FromMicrosoftDisplayId(Microsoft.UI.DisplayId displayID)
    {
        return new Windows.Graphics.DisplayId(displayID.Value);
    }
    

    See also SimpleRecorder, a Microsoft sample app.