私PowerShellだけどマウスカーソル付きのスクリーンショットを撮影したい

# 3行で要約

- PowerShellからスクリーンショットを取得するサンプル

- マウスカーソルをスクリーンショット保存する場合は自前でカーソルを描画する

- C#実装多め、PowerShell成分は控えめなサンプルです


## はじめに：マウスカーソルもスクリーンショットしたい

前回、[私PowerShellだけどスクリーンショットを撮影したい](https://qiita.com/magiclib/items/91f7d32cbd766b39b293)という記事を書いたのですが、スクリーンショットにマウスカーソルが含まれない動作になっていました。

私が知る限りでは、取扱説明書を作成する場合、細かいクリック位置を説明する場合などはマウスカーソル付きで画像保存したいことがあります。


また右クリックのコンテキストメニューのスクリーンショットを撮影する場合もマウスカーソルが無いと違和感あるのでマウスカーソルも保存したいです。

![menu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264524/364621b2-b8ce-e469-e020-6e02010ef7ec.png)


このような事情のため、マウスカーソルもスクリーンショットとして保存できるPowerShellスクリプトを作成してみました。


## マウスカーソルの撮影方法

ネットで調べたところマウスカーソルを含めて保存するAPIは提供されていないようです。
スクリーンショットの画像を作成した後で、マウスカーソルを上から描画して保存し直す方法が一般的のようです。

[GetCursorInfo()](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getcursorinfo)でマウスカーソルの情報を取得し、
[DrawIconEx()](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-drawiconex)でGDI描画することにします。


## 使用するライブラリ：SharpDXのみ縛りプレイ

前記事と同じにしても面白くないので、構成を変えます。

前回はステップ数を減らすために[ScreenCapturer](https://github.com/0x2E757/ScreenCapturer)ライブラリを採用しましたが、
今回はSharpDXライブラリのみを使用して実装するように縛りプレイすることにします。


### 実行環境構築

nugetで依存関係DLLをまとめて入手します。

:::note info
nugetコマンドがインストールされていない場合は、[こちら](https://www.nuget.org/downloads)からnuget.exeを入手してください。実行ファイルだけダウンロードできるのでインストーラーとかは不要です。
:::

以下のコマンドを実行してダウンロードしてください。

```cmd

>nuget install SharpDX.Direct3D11

(略)
Executing nuget actions took 386 ms


>>dir /b
nuget.exe
SharpDX.4.2.0
SharpDX.Direct3D11.4.2.0
SharpDX.DXGI.4.2.0


>copy /B ".\SharpDX.4.2.0\lib\netstandard1.1\SharpDX.dll" .\
        1 個のファイルをコピーしました。

>copy /B ".\SharpDX.Direct3D11.4.2.0\lib\netstandard1.1\SharpDX.Direct3D11.dll"  .\
        1 個のファイルをコピーしました。

>copy /B ".\SharpDX.DXGI.4.2.0\lib\netstandard1.1\SharpDX.DXGI.dll" .\
        1 個のファイルをコピーしました。

```


## ソースコード

以下のソースを保存して実行することでスクリーンショットを画像に保存します。
１秒間隔で１０回撮影するサンプルになっています。


```PowerShell:CaptureScreenshotWithCursor.ps1

$Refs = @(
"System.Runtime",
"System.IO",
"System.Drawing",
".\SharpDX.dll",
".\SharpDX.DXGI.dll",
".\SharpDX.Direct3D11.dll"
)

$null = [System.Reflection.Assembly]::LoadFrom(".\SharpDX.dll");
$null = [System.Reflection.Assembly]::LoadFrom(".\SharpDX.DXGI.dll");
$null = [System.Reflection.Assembly]::LoadFrom(".\SharpDX.Direct3D11.dll");

$Source = @"
using System;
using SharpDX;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

public class ScreenshotCapture : IDisposable
{
    private SharpDX.Direct3D11.Device _device;
    private OutputDuplication _duplication;
    private Texture2D _gdiImage;
    private Texture2D _destImage;
    private Texture2DDescription _textureDesc;
    private Texture2DDescription _textureGdiDesc;

    [DllImport("user32.dll")]
    private static extern bool GetCursorInfo(out CURSORINFO pci);
    [StructLayout(LayoutKind.Sequential)]
    private struct CURSORINFO
    {
        public int cbSize;
        public int flags;
        public IntPtr hCursor;
        public POINT ptScreenPos;
    }
    private struct POINT
    {
        public int X;
        public int Y;
    }
    private const int CURSOR_SHOWING = 0x00000001;

    [DllImport("user32.dll")]
    private static extern IntPtr DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyWidth, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
    private const int DI_NORMAL = 0x00000003;
    private const int DI_DEFAULTSIZE = 0x00000008;
    
    public ScreenshotCapture()
    {
        using (Factory1 _factory = new Factory1())
        {
            using (Adapter1 _adapter = _factory.GetAdapter1(0))
            {
                _device = new SharpDX.Direct3D11.Device(_adapter);
                using (Output _output = _adapter.GetOutput(0))
                {
                    using (Output1 _output1 = _output.QueryInterface<Output1>())
                    {
                        _duplication = _output1.DuplicateOutput(_device);

                        _textureDesc = new Texture2DDescription
                        {
                            CpuAccessFlags = CpuAccessFlags.Read | CpuAccessFlags.Write,
                            BindFlags = BindFlags.None,
                            Format = Format.B8G8R8A8_UNorm,
                            Width = _output.Description.DesktopBounds.Right,
                            Height = _output.Description.DesktopBounds.Bottom,
                            MipLevels = 1,
                            ArraySize = 1,
                            SampleDescription = { Count = 1, Quality = 0 },
                            Usage = ResourceUsage.Staging,
                            OptionFlags = ResourceOptionFlags.None
                        };

                        _textureGdiDesc = new Texture2DDescription
                        {
                            CpuAccessFlags = CpuAccessFlags.None,
                            BindFlags = BindFlags.RenderTarget,
                            Format = Format.B8G8R8A8_UNorm,
                            Width = _output.Description.DesktopBounds.Right,
                            Height = _output.Description.DesktopBounds.Bottom,
                            MipLevels = 1,
                            ArraySize = 1,
                            SampleDescription = { Count = 1, Quality = 0 },
                            Usage = ResourceUsage.Default,
                            OptionFlags = ResourceOptionFlags.GdiCompatible
                        };

                        _gdiImage = new Texture2D(_device, _textureGdiDesc);
                        _destImage = new Texture2D(_device, _textureDesc);
                    }
                };
            };
        };

        CaptureScreen("");  // 初回は黒画像になるので読み捨てる
    }

    public void CaptureScreen(string outputFile)
    {
        SharpDX.DXGI.Resource desktopResource = null;    // デスクトップのイメージが格納される
        OutputDuplicateFrameInformation frameInfo = new OutputDuplicateFrameInformation();

        if (_duplication.TryAcquireNextFrame(1000, out frameInfo, out desktopResource).Success)
        {
            using (Texture2D desktopImage = desktopResource.QueryInterface<Texture2D>())
            {
                _device.ImmediateContext.CopyResource(desktopImage, _gdiImage);

                using (Surface1 surface1 = _gdiImage.QueryInterface<Surface1>())
                {
                    CURSORINFO cursorInfo;
                    cursorInfo.cbSize = Marshal.SizeOf(typeof(CURSORINFO));
                    GetCursorInfo(out cursorInfo);

                    if (cursorInfo.flags == CURSOR_SHOWING)
                    {
                        DrawIconEx(surface1.GetDC(false), cursorInfo.ptScreenPos.X, cursorInfo.ptScreenPos.Y, cursorInfo.hCursor, 0, 0, 0, IntPtr.Zero, DI_NORMAL | DI_DEFAULTSIZE);
                        surface1.ReleaseDC();
                    }
                }

                _device.ImmediateContext.CopyResource(_gdiImage, _destImage);

                DataBox dataBox = _device.ImmediateContext.MapSubresource(_destImage, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None);

                using (System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(_textureDesc.Width, _textureDesc.Height, PixelFormat.Format32bppArgb))
                {
                    System.Drawing.Rectangle boundsRect = new System.Drawing.Rectangle(0, 0, _textureDesc.Width, _textureDesc.Height);
                    BitmapData mapDest = bitmap.LockBits(boundsRect, ImageLockMode.WriteOnly, bitmap.PixelFormat);
                    IntPtr sourcePtr = dataBox.DataPointer;
                    IntPtr destPtr = mapDest.Scan0;
                    for (int y = 0; y < _textureDesc.Height; y++)
                    {
                        Utilities.CopyMemory(destPtr, sourcePtr, _textureDesc.Width * 4);
                        sourcePtr = IntPtr.Add(sourcePtr, dataBox.RowPitch);
                        destPtr = IntPtr.Add(destPtr, mapDest.Stride);
                    }
                    bitmap.UnlockBits(mapDest);
                    if (!String.IsNullOrEmpty(outputFile))
                    {
                        bitmap.Save(outputFile, ImageFormat.Png);
                    }
                }
            }
            _device.ImmediateContext.UnmapSubresource(_destImage, 0);
        };
        desktopResource.Dispose();
        _duplication.ReleaseFrame();    // これを呼ばないと次で失敗する
    }

    public void Dispose()
    {
        _destImage.Dispose();
        _gdiImage.Dispose();
        _duplication.Dispose();
        _device.Dispose();
    }
}
"@

Add-Type -TypeDefinition $Source -Language CSharp -ReferencedAssemblies $Refs
$sc = New-Object -TypeName ScreenshotCapture

for($i=0; $i -lt 10; $i++){
Write-Host $i
$sc.CaptureScreen("./save" + $i + ".png")
Start-Sleep 1
}

$sc.Dispose()
$sc = $null


```


ほぼC#実装になっています。

PowerShell上でSharpDX.Direct3D11のインターフェースは取得できるのですが、いい感じにQueryInterface()できなくてTexture2Dが生成できず詰みました。PowerShellで呼べない部分だけC#にするつもりでしたが結果はこのザマです。



#### 実行例


```cmd

>powershell -NoProfile -ExecutionPolicy Unrestricted .\CaptureScreenshotWithCursor.ps1
0
1
2
3
4
5
6
7
8
9

>dir /b
nuget.exe
save0.png
save1.png
save2.png
save3.png
save4.png
save5.png
save6.png
save7.png
save8.png
save9.png
CaptureScreenshotWithCursor.ps1
SharpDX.4.2.0
SharpDX.Direct3D11.4.2.0
SharpDX.Direct3D11.dll
SharpDX.dll
SharpDX.DXGI.4.2.0
SharpDX.DXGI.dll

```


pngファイルで10ファイル保存されています。




## おわりに

ライブラリのDLLを減らしてステップ数が増えるという当然の結果になりました。

マウスカーソルも自前描画なのであまりカッコよい実装ではありません。

C#のソースが長くなったのでPowerShellスクリプト感は薄いのですが、目的は達成できたので良しとしました。



# 私PowerShellだけど…シリーズ

[私PowerShellだけど、君のタスクトレイで暮らしたい](https://qiita.com/magiclib/items/cc2de9169c781642e52d)
[私PowerShellだけど「送る」からファイルを受け取りたい（コンテキストメニュー登録もあるよ）](https://qiita.com/magiclib/items/7529557f756af9c8d21b)
[私powershellだけどタスクトレイの片隅でアイを叫ぶ](https://qiita.com/magiclib/items/9d0cd35814b6fcff2e44)
[私PowerShellだけど子を持つ親になるのはいろいろ大変そう](https://qiita.com/magiclib/items/ef0ab5c1fd8e0260b39d)
[私PowerShellだけどあなたにトーストを届けたい（プログレスバー付）](https://qiita.com/magiclib/items/12e2a9e1e1e823a7fa5c)
[私Powershellだけど日付とサイズでログを切り替えたい(log4net)](https://qiita.com/magiclib/items/4fe5bab3e879dd8de951)
[私PowerShellだけどスクリプトだけでサービス登録したい](https://qiita.com/magiclib/items/b4e2e919f02af1b04b58)
[私PowerShellだけどスクリーンショットを撮影したい](https://qiita.com/magiclib/items/91f7d32cbd766b39b293)

[サンプルソース置き場](https://github.com/part-time-penguin/qiita_powershell_samples)