Search code examples
wpfwindows-runtime.net-6.0screenshot

How to screenshot an active non-focused window using WinRT


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

Solution

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