1

I'm looking for a way to screenshot an active (non-focused) window using its IntPtr, without using user32.dll PrintWindow as it requires running the app as admin.

I was able to screen capture using WinRT following this repo with some adjustments to make it work in dotnet 6 - https://github.com/microsoft/Windows.UI.Composition-Win32-Samples/tree/master/dotnet/WPF/ScreenCapture

Now all that's left is to convert the frame (Direct3D11CaptureFrame) to an image that I can actually save. For that purpose, I found this tutorial - https://learn.microsoft.com/en-us/windows/uwp/audio-video-camera/screen-capture

It's for UWP, but the idea is to Convert the D3D11 surface into a Win2D object by doing -

// Convert our D3D11 surface into a Win2D object.
CanvasBitmap canvasBitmap = CanvasBitmap.CreateFromDirect3D11Surface(
    _canvasDevice,
    frame.Surface);

The issue is that _canvasDevice in that sample is an ICanvasResourceCreator which I don't have by following the WPF screen capture repo shared above. Iv'e tried creating a device using -

CanvasDevice.GetSharedDevice()

But using that device in the CreateFromDirect3D11Surface method throws an exception - The requested operation is not supported. (0x88990003)

If anyone has an idea how to fix that issue or achieve my goal using a different approach I'll appreciate any help.

Thanks!

Edit - Added below the entire OnFrameArrived method, where I try to convert the frame to CanvasBitmap like its done in the UWP code sample I linked above -

        private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
        {
            var newSize = false;

            using (var frame = sender.TryGetNextFrame())
            {
                var canvasDevice = CanvasDevice.GetSharedDevice();

                //Convert our D3D11 surface into a Win2D object.
                //this method below throws the exception
                _currentFrame = CanvasBitmap.CreateFromDirect3D11Surface(
                    canvasDevice,
                    frame.Surface);
                
                if (frame.ContentSize.Width != lastSize.Width ||
                    frame.ContentSize.Height != lastSize.Height)
                {
                    // The thing we have been capturing has changed size.
                    // We need to resize the swap chain first, then blit the pixels.
                    // After we do that, retire the frame and then recreate the frame pool.
                    newSize = true;
                    lastSize = frame.ContentSize;
                    swapChain.ResizeBuffers(
                        2, 
                        lastSize.Width, 
                        lastSize.Height,
                        Format.B8G8R8A8_UNorm, 
                        SwapChainFlags.None);
                }

                using (var backBuffer = swapChain.GetBackBuffer<Texture2D>(0))
                using (var bitmap = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface))
                {
                    d3dDevice.ImmediateContext.CopyResource(bitmap, backBuffer);
                }

            } // Retire the frame.

            swapChain.Present(0, PresentFlags.None);

            if (newSize)
            {
                framePool.Recreate(
                    device,
                    DirectXPixelFormat.B8G8R8A8UIntNormalized,
                    2,
                    lastSize);
            }
        }
    ```
Ben
  • 793
  • 2
  • 13
  • 29
  • Do you have a full reproducing sample as UWP? https://stackoverflow.com/help/minimal-reproducible-example – Simon Mourier Mar 20 '23 at 13:01
  • @SimonMourier, the error I experience is in WPF. The link I shared has a working sample in UWP. I'm trying to mimic that behavior in my WPF version of that. – Ben Mar 20 '23 at 18:10
  • Iv'e edited my question and added the full method code at where the exception is thrown. A full reproduced repo is available in the WPF screen capture sample link I shared. – Ben Mar 20 '23 at 19:08
  • Why do you want to use Win2D (Direct2D wrapper) to save a bitmap file, if that's what you're trying to do (it's still unclear)? You can use WinRT's classes in https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.imaging – Simon Mourier Mar 21 '23 at 07:23
  • Could you please explain it further? "Why do you want to use Win2D" - cuz looking into the Microsoft code samples I shared above, this is the way to take screenshots using WinRT. I need to somehow convert `Direct3D11CaptureFrame` into an image I can save on disk. – Ben Mar 21 '23 at 09:44

1 Answers1

1

You don't need Direct2D (Win2D is a wrapper over Direct2D) to get the bytes from a surface. You can get the bytes using standard DirectX API, then write them to a file using the Bitmap API you want.

Here is an example using WinRT's SoftwareBitmap and BitmapEncoder, but you can use GDI+ (System.Drawing.Bitmap, WIC, etc.) to save a file.

Starting from the BasicCapture.cs file, modify it like this:

public class BasicCapture : IDisposable
{
    ...
    private Texture2D cpuTexture; // add this
    private bool saved = false; // add this

    public BasicCapture(IDirect3DDevice d, GraphicsCaptureItem i)
    {
        ...
        cpuTexture = CreateTexture2D(item.Size.Width, item.Size.Height); // add this
    }

    public void Dispose()
    {
        session?.Dispose();
        framePool?.Dispose();
        swapChain?.Dispose();
        d3dDevice?.Dispose();
        cpuTexture?.Dispose(); // add this
    }

    private Texture2D CreateTexture2D(int width, int height)
    {
        // create add texture2D 2D accessible by the CPU
        var desc = new Texture2DDescription()
        {
            Width = width,
            Height = height,
            CpuAccessFlags = CpuAccessFlags.Read,
            Usage = ResourceUsage.Staging,
            Format = Format.B8G8R8A8_UNorm,
            ArraySize = 1,
            MipLevels = 1,
            SampleDescription = new SampleDescription(1, 0),
        };
        return new Texture2D(d3dDevice, desc);
    }

    private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
    {
        using (var frame = sender.TryGetNextFrame())
        {
            ...

            using (var backBuffer = swapChain.GetBackBuffer<SharpDX.Direct3D11.Texture2D>(0))
            using (var bitmap = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface))
            {
                d3dDevice.ImmediateContext.CopyResource(bitmap, backBuffer);
                
                // add this to copy the DirectX resource into the CPU-readable texture2D
                d3dDevice.ImmediateContext.CopyResource(bitmap, cpuTexture);

                // now,  this is just an example that only saves the first frame
                // but you could also use
                // d3dDevice.ImmediateContext.MapSubresource(cpuTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None, out var stream); and d3dDevice.ImmediateContext.UnMapSubresource
                // to get the bytes (out from the returned stream)
                if (!_saved)
                {
                    _saved = true;
                    Task.Run(async () =>
                    {
                        // get IDirect3DSurface from texture (from WPF sample's helper code)
                        var surf = Direct3D11Helper.CreateDirect3DSurfaceFromSharpDXTexture(cpuTexture);
                        
                        // build a WinRT's SoftwareBitmap from this surface/texture
                        var softwareBitmap = await SoftwareBitmap.CreateCopyFromSurfaceAsync(surf);
                        using (var file = new FileStream(@"c:\temp\test.png", FileMode.Create, FileAccess.Write))
                        {
                            // create a PNG encoder
                            var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, file.AsRandomAccessStream());
                            
                            // set the bitmap to it & flush
                            encoder.SetSoftwareBitmap(softwareBitmap);
                            await encoder.FlushAsync();
                        }
                    });
                }
            }
        }
    }
}
Ben
  • 793
  • 2
  • 13
  • 29
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Ty for that elaborated answer and an easy-to-follow code. Works great, cheers Simon – Ben Mar 22 '23 at 13:24
  • Simon, do you happen to know why the logic above wouldn't work when running on a background thread? If I try to start the capture using Task.Run() a new frame is never arrived. Running it on the main thread causes issues with UI blocking. – Ben Apr 03 '23 at 20:00
  • @ben - Depends on details but have you initialized your thread as STA (vs MTA which is the default state), otherwise ask another question with reproducible code. – Simon Mourier Apr 04 '23 at 05:33
  • I read about STA vs MTA, I do think that's part of the issue but not completely sure how to uttlize it for my use-case. Created a new question with reproducible code including some of what we spoke of here to add the screenshot capability. If you get the chance to look at it - https://stackoverflow.com/q/75940867/7210967 Thx! – Ben Apr 05 '23 at 14:55