Technically Impossible

Lets look at the weak link in your statement. Anything "Technically Impossible" basically means we haven't figured out how yet.

C#でのスクリーンショット - 画面全体、特定領域、そして最前面Windowのキャプチャ

Zoom Meetingsをはじめとするビデオ・コミュニケーションが、日常利用において何も例外的な存在ではない時代となった。それはオンライン・ミーティングに限らず、大人数が出席する、いわゆるカンファレンスでも用いられている。

このような時、あると便利なのが画面キャプチャ、スクリーンショットのためのツールだ。特にプレゼンテーション資料が配布されない場合、今まさに移っている画面をメモ代わりにキャプチャする。

Windows標準の操作では、スクリーンショットを取り、画像編集ソフトにコピーし、画像ファイルとして保存する、という手順を経る必要がある。これをボタン一つで、ファイル出力まで完結させたいと思い、とあるフリーウェアを使っていた。そして、これが気づかないうちに無反応になるときがあるのだ。
この現象があまりに頻発するので、ツールを自作することにした。そして作業をしているうちに気付くのだ。スクリーンキャプチャ・ツールは、イベントドリブンなライブラリ呼び出しで完結できるものではなく、Win32 API呼び出しが避けられないことに。それは次の場面で必要とされる。

  1. Active Window(最前面のWindow)を検出する。
  2. 特定キー押下のタイミングを検出する。

この投稿では、1の話題を含めたスクリーンショットを取得するためのコードを紹介する。
なお、ソースコード全体は投稿末尾に掲載している。

スクリーンショットとキャプチャ

そもそもの根拠が不明なのだが、スクリーンショットとキャプチャは厳密には違うらしい。

スクリーンショット 画面全体のスナップショット
キャプチャ 特定領域のスナップショット

撮影対象に関わらず、この投稿では「キャプチャ」に統一する。

典型的手順

撮影対象に関わらず、

  • スクリーン全体
  • 最前面のWindow
  • 画面上に指定した矩形領域

キャプチャのための手順は同じだ。端的に表現すれば、画面全体の中から、指定した矩形領域を切り出しているに過ぎない。典型的な処理は、次のコードに含まれる関数「snap」に集約されている。

  1. 指定された矩形(Rectangle my_rectangle)と同じ大きさのビットマップを作成し、
  2. 画面から指定された矩形をコピーし、
  3. 画像として保存する。
String filename(String val)
{
    String my_dir = "d:\\user temp\\" + DateTime.Now.ToString("yyyyMMdd") + "\\";
    String my_file = DateTime.Now.ToString("hhmmss") + DateTime.Now.Millisecond.ToString() + ".jpg";
    return (my_dir + val + my_file);

}

public void snap(Rectangle my_rectangle)
{
    Bitmap my_bmp = new Bitmap(my_rectangle.Width, my_rectangle.Height);
    Graphics my_graphics = Graphics.FromImage(my_bmp);

    my_graphics.CopyFromScreen(my_rectangle.X, my_rectangle.Y, 0, 0, my_rectangle.Size);
    my_bmp.Save(filename(""), System.Drawing.Imaging.ImageFormat.Jpeg);
}

つまり、「my_rectangle」にスクリーン全体を指定すれば、画面全体をキャプチャできるし、最前面Windowの座標、大きさを指定すれば、指定したWindowをキャプチャできる。
適当な座標、大きさを指定すれば、その矩形領域をキャプチャできる。

スクリーン全体を指定するのは簡単だ。関数「snap」へオブジェクトをそのまま引き渡せばよい。

snap(Screen.PrimaryScreen.Bounds);

特定の矩形領域を指定するのも簡単だ。例えば画面右上隅を500×500でキャプチャしようと思えば、次のように指定する。コードに続く画像が、出力した画像サンプルだ。

var x_width = 500;
var y_height = 500;

WindowCoordinate wc;

wc.upper_left_x = Screen.PrimaryScreen.Bounds.Width - x_width;
wc.upper_left_y = 0;
wc.bottom_right_x = Screen.PrimaryScreen.Bounds.Width;
wc.bottom_right_y = Screen.PrimaryScreen.Bounds.Height - y_height;

Rectangle my_rectangle = new Rectangle(wc.upper_left_x, wc.upper_left_y, x_width, y_height);

snap(my_rectangle);

問題は、最前面Windowのキャプチャだ。これが少々曲者なのだ。

素直に最前面Windowをキャプチャする。

まず素直に対応する。とはいえ、処理対象として最前面Windowを捉えるには、Win32 APIの呼び出しが避けられない。

対象となるWindowを捉え、その矩形情報を関数「snap」へ引き渡すまでのコードは次のようになる。

Windowsは、各Windowに管理番号を割り当てている。この管理番号をハンドルと言う。「GetForegroundWindow」で最前面Windowのハンドルを取得し、その矩形情報を関数「snap」に引き渡している。

矩形構造体、Win32 APIの宣言部分

[StructLayout(LayoutKind.Sequential)]

private struct WindowCoordinate
{
    public int upper_left_x;
    public int upper_left_y;
    public int bottom_right_x;
    public int bottom_right_y;
}

[DllImport("user32.Dll")]
static extern int GetWindowRect(IntPtr hWnd, out WindowCoordinate rect);


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

[DllImport("dwmapi.dll")]
extern static int DwmGetWindowAttribute(IntPtr hWnd, int dwAttribute, out WindowCoordinate rect, int cbAttribute);


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


WindowCoordinate wc;

IntPtr window_handle = GetForegroundWindow();
GetWindowRect(window_handle, out wc);

var rectangle_width = wc.bottom_right_x - wc.upper_left_x;
var rectangle_height = wc.bottom_right_y - wc.upper_left_y;
Rectangle my_rectangle = new Rectangle(wc.upper_left_x, wc.upper_left_y, rectangle_width, rectangle_height);

snap(my_rectangle);

このコードが出力する画像サンプルが、次の画像だ。最前面Windowの裏側に位置している画面が、四辺にほんのわずかに含まれているのが分かる。これを排除するのが次の課題だ。

DWM (Desktop Window Manager)で最前面Windowをキャプチャする。

Windows Vista以降に加えられた演出効果により、それらを省いた、Windowそのものの矩形情報を取得するには、Windowそのものの属性値を取得する必要があり、それはWindow Managerを通じて参照する。
そのため「GetWindowRect」の代わりに、「DwmGetWindowAttribute」を参照する。このとき、Windowそのものの境界情報を取得するため、定数「DWMWA_EXTENDED_FRAME_BOUNDS」を指定する。
後の処理は同じだ。きちんと縁取りされたキャプチャ画像が出力される。

int DWMWA_EXTENDED_FRAME_BOUNDS = 9;
DwmGetWindowAttribute(window_handle, DWMWA_EXTENDED_FRAME_BOUNDS, out wc, Marshal.SizeOf(typeof(WindowCoordinate)));


実装例 - 1 Push Snap

1 Push Snapは、windowキャプチャからファイル保存までを1ボタンの操作に集約したツールだ。ここで紹介しているコードとイベントフック*1を組み合わせて作成した。GitHubでコードを公開しているので、関心があれば参照してほしい。
impsbl.hatenablog.jp