【C#】Windows用のスクリーンショットアプリを作成してみた

#概要
皆様は、Windowsでスクリーンショット（以下、スクショ）を撮るときに何を使いますか？
デフォルトで入ってる機能として、主に以下のものがあります。

`Print Screen`
すべてのディスプレイに対して、一枚の画像としてスクショします。
ペイントなどに貼り付けて保存する必要があります。

`Alt + Print Screen`
アクティブなウィンドウに対してスクショします。
ペイントなどに貼り付けて保存する必要があります。

`Win + Print Screen`
すべてのディスプレイに対して、一枚の画像としてスクショします。
自動で保存され、デフォルトでは「C:\Users\Username\Pictures\Screenshots」に保存されます。

`Win + Alt + Print Screen`
アクティブなウィンドウに対してスクショします。
自動で保存され、デフォルトでは「C:\Users\Username\Videos\Captures」に保存されます。

使い勝手のいいのは、下2つでしょうか。
しかし、キーが押しづらく、使い勝手もいまいちだったので、自分で同様以上のことができるアプリ作ろうと思いました。


***最後に配布URLを貼っています。気になった方は是非使ってみてください。***

#仕様
まず、キモとなる技術的な裏付けとして、`フォーム外のキー入力を感知できるか`を調べました。
できることが分かったので、作れる確信を持ちました。

メインの仕様として、作る前に以下を決めました。

- キー1つでスクショが撮れる
- スクショするディスプレイまたはアクティブウィンドウを選択できる
- 自動で保存する
- タスクトレイ表示し、常駐アプリとする

サブの仕様として、作りながら以下を決めました。

- 保存先のフォルダを設定できる
- スクショした後、ペイントで開くように設定できる
- スクショをするキーを設定できる
- 保存する拡張子を設定できる
- 保存するファイル名のテンプレートを設定できる


![シャッターアイコン5 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/c219c123-6402-cced-4a9a-a71077ffc634.png)
アイコンは、[こちら](https://icooon-mono.com/14803-%e3%82%b7%e3%83%a3%e3%83%83%e3%82%bf%e3%83%bc%e3%82%a2%e3%82%a4%e3%82%b3%e3%83%b35/)のフリー素材を使用しました。

![20200615_225814.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/31526488-2ec9-ba4e-2c70-868ad3a784af.png)
タスクトレイの表示

![20200615_225910.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/b8aa6cbf-8f67-3ab7-54cd-ab718370ac6f.png)
メニューとツールチップ
デフォルトでは、`Pause`キーでスクショができます。
`(P)`は、PrimaryDisplayです。

![20200615_230052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/ad0b41b5-9849-3d4e-1c60-7c4d7ee3337d.png)
設定画面

#プログラム
アプリは、C#(.Net)で Windows Form Application として作成しました。
以下、機能を実装するのに調べたことをまとめます。

###フォーム外のキーイベントの感知
フォーム外のキーイベントを監視することを、`キーロガー`というそうです。
メインフォームに、`Timer`コントロールを追加して監視します。

```.cs
private void timer1_Tick(object sender, EventArgs e) {
    var key_state = KeyStateBackgroundWatcher.IsKeyLocked(Keys.Pause);

    if (key_state) {
        Console.WriteLine("Keys.Pause");
    }
}

// WindowsAPIのインポート
[System.Runtime.InteropServices.DllImport("user32.dll",
    CharSet = System.Runtime.InteropServices.CharSet.Auto,
    CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
public static extern short GetKeyState(int nVirtKey);

public static bool IsKeyLocked(Keys key_val) {
    return (GetKeyState((int)key_val) & 0x80) != 0;
}
```
参考：https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q12189853917

###アプリをタスクトレイのみに表示する
```Program.cs
static class Program {
    /// <summary>
    /// アプリケーションのメイン エントリ ポイントです。
    /// </summary>
    [STAThread]
    static void Main() {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        // 画面を表示せず、アプリケーションを実行します。
        //Application.Run(new MainForm());
        var form = new MainForm();
        Application.Run();
    }
}
```

`NotifyIcon`コントロールを追加します。メニューはこの`NotifyIcon`に付属させます。
以下は、メニューの`Exit`を押したときのイベントです。
アプリを終了する前に、`notifyIcon1.Dispose()`をすることで、タスクトレイ上にアイコンが残らないようにします。

```.cs
private void tsmi_exit_Click(object sender, EventArgs e) {
    notifyIcon1.Dispose();
    Application.Exit();
}
```

###アクティブウィンドウのスクショを撮る
以下は、ウィンドウの影を含めないで、キャプチャする方法です。
※アクティブウィンドウは、立体感を出すためかウィンドウ周りに影が入っていて、通常はそれを含めてキャプチャしてしまいます。


```.cs
private static Bitmap CaptureActiveWindow() {
    //アクティブなウィンドウのデバイスコンテキストを取得
    IntPtr hWnd = NativeMethods.GetForegroundWindow();
    IntPtr winDC = NativeMethods.GetWindowDC(hWnd);
    //ウィンドウの大きさを取得
    NativeMethods.RECT winRect = new NativeMethods.RECT();
    NativeMethods.DwmGetWindowAttribute(
        hWnd,
        NativeMethods.DWMWA_EXTENDED_FRAME_BOUNDS,
        out var bounds,
        Marshal.SizeOf(typeof(NativeMethods.RECT)));
    NativeMethods.GetWindowRect(hWnd, ref winRect);
    //Bitmapの作成
    var offsetX = bounds.left - winRect.left;
    var offsetY = bounds.top - winRect.top;
    Bitmap bmp = new Bitmap(bounds.right - bounds.left, bounds.bottom - bounds.top);

    //Graphicsの作成
    using (var g = Graphics.FromImage(bmp)) {
        //Graphicsのデバイスコンテキストを取得
        IntPtr hDC = g.GetHdc();
        //Bitmapに画像をコピーする
        Console.WriteLine(winRect);
        NativeMethods.BitBlt(hDC, 0, 0, bmp.Width, bmp.Height, winDC, offsetX, offsetY, NativeMethods.SRCCOPY);
        //解放
        g.ReleaseHdc(hDC);
    }
    NativeMethods.ReleaseDC(hWnd, winDC);

    return bmp;
}

private static void screenShot_Active() {
    using (var bmp = CaptureActiveWindow()) {
        bmp.Save(@"hoge\image.png", ImageFormat.Png);
    }
}
```
```NativeMethods.cs
class NativeMethods {
    public const int SRCCOPY = 13369376;
    public const int DWMWA_EXTENDED_FRAME_BOUNDS = 9;

    [DllImport("user32.dll")]
    public static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("gdi32.dll")]
    public static extern int BitBlt(IntPtr hDestDC,
        int x, int y, int nWidth, int nHeight,
        IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);

    [DllImport("user32.dll")]
    public static extern IntPtr ReleaseDC(IntPtr hwnd, IntPtr hdc);
    [StructLayout(LayoutKind.Sequential)]
    public struct RECT {
        public int left; public int top; public int right; public int bottom;
    }

    [DllImport("user32.dll")]
    public static extern IntPtr GetWindowDC(IntPtr hwnd);

    [DllImport("user32.dll")]
    public static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll")]
    public static extern int GetWindowRect(IntPtr hwnd, ref RECT lpRect);

    [DllImport("dwmapi.dll")]
    public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);
}
```
参考：https://teratail.com/questions/103093

###すべてのディスプレイを1枚スクショとして撮る

```.cs
private static string screenShot_All() {
    int left = SystemInformation.VirtualScreen.Left;
    int top = SystemInformation.VirtualScreen.Top;
    int width = SystemInformation.VirtualScreen.Width;
    int hight = SystemInformation.VirtualScreen.Height;

    Rectangle rect = new Rectangle(left, top, width, hight);
    using (var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb)) {
        using (var g = Graphics.FromImage(bmp)) {
            g.CopyFromScreen(rect.X, rect.Y, 0, 0, rect.Size, CopyPixelOperation.SourceCopy);
        }
        bmp.Save(@"hoge\image.png", ImageFormat.Png);
    }
}
```

あとは、設定の読み込み処理や保存処理などをちょこちょこ書けば終わりです。

#バグ
アクティブウィンドウを取得する場合、対象がブラウザだとうまく取得できません。
以下に、（推測みたいですが）理由がありました。
[Unity×Windowsの画面キャプチャで四苦八苦してる話(1)](https://qiita.com/AtsushiYamashita/items/2a1cdebe74f0d5c27da3)

`user32.dll`を使用している場合、うまく取得できないウィンドウがあるみたいですね...:cold_sweat:

#配布
以下で配布しています。（配布は、予告なく終了する可能性があります）
zipを解凍したら、フォルダごとどこかに置いて、`ScreenShotApp.exe`を起動してください。

https://github.com/nemutas/ScreenShotApp/releases