C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드
이전 글에서 DXGI를 이용한 화면 캡처 소스 코드를 C++로 알아봤는데요.
DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)
; https://www.sysnet.pe.kr/2/0/11385
이번엔 C#으로 옮겨봤습니다. 물론 이를 위해 DirectX를 위한 Interop 라이브러리가 필요한데요, 바로 SharpDX가 그런 역할을 합니다.
A new managed .NET/C# Direct3D 11 API generated from DirectX SDK headers 
; http://code4k.blogspot.kr/2010/10/managed-netc-direct3d-11-api-generated.html
NuGet에도 배포되어 있는 데다,
SharpDX
; https://www.nuget.org/packages/SharpDX/
SharpDX.DXGI
; https://www.nuget.org/packages/SharpDX.DXGI/4.1.0-ci184
github에 소스 코드와 그 예제 코드가 모두 공개되어 있습니다. 그중에는 화면 캡처 예제도 있습니다.
SharpDX-Samples/Desktop/Direct3D11.1/ScreenCapture/Program.cs 
; https://github.com/sharpdx/SharpDX-Samples/blob/master/Desktop/Direct3D11.1/ScreenCapture/Program.cs
Nuget을 통해 SharpDX를 참조하면,
Install-Package SharpDX.Direct3D11 -Version 4.0.1 
Install-Package SharpDX.DXGI -Version 4.0.1
각각 다음의 DLL을 얻게 됩니다.
SharpDX.dll
SharpDX.DXGI.dll
SharpDX.Direct3D11.dll
이를 이용해 "
DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)" 글의 DXGIManager, DXGIOutputDuplication 클래스를 각각 C#으로 다음과 같이 작성할 수 있습니다.
// DXGIManager.cs
using SharpDX;
using SharpDX.DXGI;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
namespace WindowsFormsApp1
{
    public class DXGIManager : IDisposable
    {
        // ...[생략]...
        public DXGIManager(CaptureSource source)
        {
            Initialize(source);
        }
        void Initialize(CaptureSource captureSource)
        {
            _captureSource = captureSource;
            _factory = new Factory4();
            _outputs = new List<DXGIOutputDuplication>();
            int vgaCardCount = _factory.GetAdapterCount();
            foreach (Adapter adapter in _factory.Adapters)
            {
                List<Output> outputs = new List<Output>();
                foreach (Output output in adapter.Outputs)
                {
                    OutputDescription desc = output.Description;
                    if (desc.IsAttachedToDesktop == false)
                    {
                        continue;
                    }
                    outputs.Add(output);
                }
                if (outputs.Count == 0)
                {
                    continue;
                }
                SharpDX.Direct3D11.Device device = new SharpDX.Direct3D11.Device(adapter);
                foreach (Output output in outputs)
                {
                    using (Output1 output1 = output.QueryInterface<Output1>())
                    {
                        OutputDuplication outputDuplication = output1.DuplicateOutput(device);
                        if (outputDuplication == null)
                        {
                            continue;
                        }
                        _outputs.Add(
                            new DXGIOutputDuplication(adapter, device, outputDuplication, output1.Description));
                    }
                }
                // ...[생략]...
            }
            if (this.Initialized == true)
            {
                CalcOutputRect();
            }
        }
        public bool Capture(byte[] buf, int timeout)
        {
            foreach (DXGIOutputDuplication dupOutput in GetOutputDuplicationByCaptureSource())
            {
                Rectangle desktopBounds = dupOutput.DesktopCoordinates;
                if (dupOutput.AcquireNextFrame(timeout, copyBuffer, buf) == false)
                {
                    return false;
                }
            }
            return true;
        }
        private void copyBuffer(Surface1 surface1, Rectangle desktopBounds, byte[] buf)
        {
            if (surface1 == null)
            {
                return;
            }
            DataRectangle map = surface1.Map(MapFlags.Read);
            GCHandle pinnedArray = GCHandle.Alloc(buf, GCHandleType.Pinned);
            IntPtr dstPtr = pinnedArray.AddrOfPinnedObject();
            IntPtr srcPtr = map.DataPointer;
            int height = desktopBounds.Height;
            int width = desktopBounds.Width;
            Rectangle offsetBounds = desktopBounds;
            offsetBounds.Offset(-this._outputRect.Left, -this._outputRect.Top);
            {
                for (int y = 0; y < height; y++)
                {
                    Utilities.CopyMemory(dstPtr + (offsetBounds.Left) * 4, srcPtr, width * 4);
                    srcPtr = IntPtr.Add(srcPtr, map.Pitch);
                    dstPtr = IntPtr.Add(dstPtr, this.Width * 4);
                }
            }
            pinnedArray.Free();
            surface1.Unmap();
        }
        // ...[생략]...
        private List<DXGIOutputDuplication> GetOutputDuplicationByCaptureSource()
        {
            List<DXGIOutputDuplication> list = new List<DXGIOutputDuplication>();
            int nthMonitor = 0;
            foreach (DXGIOutputDuplication output in _outputs)
            {
                switch (_captureSource)
                {
                    case CaptureSource.Monitor1:
                        if (output.IsPrimary() == true)
                        {
                            list.Add(output);
                        }
                        break;
                    case CaptureSource.Monitor2:
                        if (output.IsPrimary() == false)
                        {
                            list.Add(output);
                        }
                        break;
                    case CaptureSource.Monitor3:
                        if (output.IsPrimary() == false)
                        {
                            nthMonitor++;
                        }
                        if (nthMonitor == ((int)CaptureSource.Monitor3) - 1)
                        {
                            list.Add(output);
                        }
                        break;
                    case CaptureSource.Desktop:
                        list.Add(output);
                        break;
                }
                if (_captureSource != CaptureSource.Desktop && list.Count == 1)
                {
                    break;
                }
            }
            return list;
        }
        // ...[생략]...
    }
}
// DXGIManager.cs
using SharpDX;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.Mathematics.Interop;
using System;
using System.Drawing;
using System.Runtime.InteropServices;
namespace WindowsFormsApp1
{
    class DXGIOutputDuplication
    {
        Adapter _adapter;
        SharpDX.Direct3D11.Device _device;
        SharpDX.Direct3D11.DeviceContext _deviceContext;
        OutputDuplication _outputDuplication;
        OutputDescription _description;
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfoEx lpmi);
        public DXGIOutputDuplication(Adapter adapter,
            SharpDX.Direct3D11.Device device,
            OutputDuplication outputDuplication, OutputDescription description)
        {
            _adapter = adapter;
            _device = device;
            _deviceContext = _device.ImmediateContext;
            _outputDuplication = outputDuplication;
            _description = description;
        }
        // ...[생략]...
        internal bool AcquireNextFrame(int timeout, Action copyAction, byte[] buf)
        {
            OutputDuplicateFrameInformation fi;
            SharpDX.DXGI.Resource desktopResource = null;
            try
            {
                _outputDuplication.AcquireNextFrame(timeout, out fi, out desktopResource);
            }
            catch (SharpDXException e)
            {
                if (e.ResultCode == DXGIError.DXGI_ERROR_ACCESS_LOST)
                {
                    throw;
                }
                return false;
            }
            if (desktopResource == null)
            {
                return false;
            }
            try
            {
                using (Texture2D textureResource = desktopResource.QueryInterface())
                {
                    Texture2DDescription desc = textureResource.Description;
                    Texture2DDescription textureDescription = desc;
                    textureDescription.MipLevels = 1;
                    textureDescription.ArraySize = 1;
                    textureDescription.SampleDescription.Count = 1;
                    textureDescription.SampleDescription.Quality = 0;
                    textureDescription.Usage = ResourceUsage.Staging;
                    textureDescription.BindFlags = 0;
                    textureDescription.CpuAccessFlags = CpuAccessFlags.Read;
                    textureDescription.OptionFlags = ResourceOptionFlags.None;
                    using (Texture2D d3d11Texture2D = new Texture2D(_device, textureDescription))
                    {
                        _device.ImmediateContext.CopyResource(textureResource, d3d11Texture2D);
                        using (Surface1 surface = d3d11Texture2D.QueryInterface())
                        {
                            copyAction(surface, this.DesktopCoordinates, buf);
                            return true;
                        }
                    }
                }
            }
            finally
            {
                if (desktopResource != null)
                {
                    desktopResource.Dispose();
                }
                _outputDuplication.ReleaseFrame();
            }
        }
        // ...[생략]...
    }
}
첨부한 파일은 위의 예제 코드를 모두 포함, 동작하는 프로젝트입니다. 실행하면 윈도우가 하나 뜨는데, 그 윈도우에 포커스를 두고 Ctrl + C키를 누르면 1번 모니터의 화면을 캡처해서 윈도우에 출력합니다. 이렇게!
참고로, 그래픽 카드 제조사 측에서 제공하는 화면 캡처 SDK도 있습니다. (언어는 C++입니다.)
NVIDIA Capture SDK
; https://developer.nvidia.com/capture-sdk
[PDF] NVIDIA CAPTURE SDK PROGRAMMING GUIDE
; http://developer.download.nvidia.com/designworks/capture-sdk/docs/6.1/NVIDIA-Capture-SDK-Programming-Guide.pdf
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]