きな粉もち.net

.NET関連仕事に携わっています。OSSのソースを読んで気がついたことを中心に呟いたりブログに投稿したりしています。最近はUiPathを使ったRPAも研究中。気軽にフォローやツッコミよろしくおねがいします! Gitはここを使っています https://github.com/kinakomotitti

UWP × Windows.Graphics.Capture × スクリーンショットの取得サンプルを作ってみた

この記事の目的

この記事では、
UWPでスクリーンショットを取得するまでの手順をまとめること
を目的としています。

本題

★背景

WPFスクリーンショットを取得するアプリを作っていましたが、
配布をすることと、興味があったことから、
UWPで同じことができるアプリを作ってみようと思ったのがきっかけです。

UWPでスクリーンショットを取得する方法を調べてみると、Docsでちょうどよい記事を見つけました。
docs.microsoft.com


よく読んでみると、UWPでスクリーンショットを取得する機能は、
Windows 10 Version 1803で新しく追加された機能らしいです。

タイミングが良すぎましたw
記事に従ってちゃかっと実装完了♪



とか思っていましたが、そう簡単にいかなかったです。
ということで、公式の手順と、自分なりに試行錯誤した手順をまとめてみました。



★準備・その1

当たり前ですが、Visual Studio を開き、UWPのプロジェクトを作成しておきます。
何でもいいですが、わかりやすいように空白のアプリを選択します。
f:id:kinakomotitti:20180523220257p:plain


★準備・その2

ここは、公式の手順にある通りです。

1. Right-click Package.appxmanifest in the Solution Explorer.
2. Select Open With...
3. Choose XML (Text) Editor.
4. Select OK.
5. In the Package node, add the following attribute: xmlns:uap6="http://schemas.microsoft.com/appx/manifest/uap/windows10/6"
6. Also in the Package node, add the following to the IgnorableNamespaces attribute: uap6
7. In the Capabilities node, add the following element: 

5.6.を実装後は以下のようになります。
f:id:kinakomotitti:20180523220304p:plain



7.を実装後は以下のようになります。
f:id:kinakomotitti:20180523220311p:plain



マニュフェストファイルって、コードを直接編集するのですね。
GUIからしか操作したことがなかったので、少し新鮮な気持ちでした。


★準備・その3

対象のプロジェクトに、NugetでWin2D.uwpをインストールします。
f:id:kinakomotitti:20180523220325p:plain



★サンプルコード

MainPage.xaml.csに以下のコードを追加します。
長いコードなのはご容赦ください。。。
公式コードをベースとしていますが、
エラーが出ていたところを修正しています。
また、出力結果の確認をするために、スクリーンショットpng形式で出力する実装を追加しました。

using Microsoft.Graphics.Canvas;
using System;
using System.Threading.Tasks;
using Windows.Graphics;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX;
using Windows.Storage;
using Windows.UI.Composition;
using Windows.UI.Xaml.Controls;

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        // Capture API objects.
        private SizeInt32 _lastSize;
        public GraphicsCaptureItem item;
        private GraphicsCaptureItem _item;
        private Direct3D11CaptureFramePool _framePool;
        private GraphicsCaptureSession _session;

        // Non-API related members.
        private CanvasDevice _canvasDevice;
        private CompositionDrawingSurface _surface;

        public MainPage()
        {
            this.InitializeComponent();
            StartCaptureAsync();
        }

        public async Task StartCaptureAsync()
        {
            // The GraphicsCapturePicker follows the same pattern the 
            // file pickers do. 
            var picker = new GraphicsCapturePicker();
            GraphicsCaptureItem item = await picker.PickSingleItemAsync();

            // The item may be null if the user dismissed the 
            // control without making a selection or hit Cancel. 
            if (item != null)
            {
                StartCaptureInternal(item);
            }
        }

        private void StartCaptureInternal(GraphicsCaptureItem item)
        {
            // Stop the previous capture if we had one.
            StopCapture();

            _item = item;
            _lastSize = _item.Size;
   
            //初期化がないとPoolのCreateで落ちるので追加。
            _canvasDevice = new CanvasDevice(); 
            _framePool = Direct3D11CaptureFramePool.Create(
               _canvasDevice, // D3D device 
               DirectXPixelFormat.B8G8R8A8UIntNormalized, // Pixel format 
               2, // Number of frames 
               _item.Size); // Size of the buffers 

            _framePool.FrameArrived += (s, a) =>
            {
                // The FrameArrived event is raised for every frame on the thread
                // that created the Direct3D11CaptureFramePool. This means we 
                // don't have to do a null-check here, as we know we're the only 
                // one dequeueing frames in our application.  

                // NOTE: Disposing the frame retires it and returns  
                // the buffer to the pool.

                using (var frame = _framePool.TryGetNextFrame())
                {
                    ProcessFrame(frame);
                }
            };

            _item.Closed += (s, a) =>
            {
                StopCapture();
            };

            _session = _framePool.CreateCaptureSession(_item);
            _session.StartCapture();
        }

        public void StopCapture()
        {
            _session?.Dispose();
            _framePool?.Dispose();
            _item = null;
            _session = null;
            _framePool = null;
        }

        private async Task ProcessFrame(Direct3D11CaptureFrame frame)
        {
            // Resize and device-lost leverage the same function on the
            // Direct3D11CaptureFramePool. Refactoring it this way avoids 
            // throwing in the catch block below (device creation could always 
            // fail) along with ensuring that resize completes successfully and 
            // isn’t vulnerable to device-lost.   
            bool needsReset = false;
            bool recreateDevice = false;

            if ((frame.ContentSize.Width != _lastSize.Width) ||
                (frame.ContentSize.Height != _lastSize.Height))
            {
                needsReset = true;
                _lastSize = frame.ContentSize;
            }

            try
            {
                // Take the D3D11 surface and draw it into a  
                // Composition surface.

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

               //出力結果を確認するために、ファイルを「Pictures」フォルダに出力するように変更。
               //ここから
                string filename = $"{ DateTime.Now.ToString("yyyMMdd_HHmmss.fffffff")}.png";
                StorageFolder pictureFolder = KnownFolders.SavedPictures;
                var file = await pictureFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
               //ここまで

                using (var fileStream = await file.OpenAsync(FileAccessMode.ReadWrite))
                {
                    await canvasBitmap.SaveAsync(fileStream, CanvasBitmapFileFormat.Png, 1f);
                }
                // Helper that handles the drawing for us, not shown. 
                //FillSurfaceWithBitmap(_surface, canvasBitmap);
            }
            // This is the device-lost convention for Win2D.
            catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult))
            {
                // We lost our graphics device. Recreate it and reset 
                // our Direct3D11CaptureFramePool.  
                needsReset = true;
                recreateDevice = true;
            }

            if (needsReset)
            {
                ResetFramePool(frame.ContentSize, recreateDevice);
            }
        }
       //公式コードでは、第一引数のsizeは「Vector2」型で定義されていましたが、
  //コンパイルが通らないので、「SizeInt32」型に変更しました。
        private void ResetFramePool(SizeInt32 size, bool recreateDevice)
        {
            do
            {
                try
                {
                    if (recreateDevice)
                    {
                        _canvasDevice = new CanvasDevice();
                    }

                    _framePool.Recreate(
                        _canvasDevice,
                        DirectXPixelFormat.B8G8R8A8UIntNormalized,
                        2,
                        size);
                }
                // This is the device-lost convention for Win2D.
                catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult))
                {
                    _canvasDevice = null;
                    recreateDevice = true;
                }
            } while (_canvasDevice == null);
        }
    }
}

まとめ

UWPでスクリーンショットが取得できる。
スクリーンショット機能は、Windows10 1803から。
Windows10のバージョンに合わせて、SDKのインストールも忘れずに行う必要がある。