Neutral Scent

App developments & Gadgets

WPFでマウスで選択した範囲をスクリーンキャプチャーする


どうも、クラスのprivateメンバー変数はlowerCamelCaseで、_は付けない派 id:kaorunです。
最近は間違った事を書いて質問に答えてもらうメソッドが巷で流行っているらしいですが、それはともかく、今回はWPFでさらっとスクリーンキャプチャをするためのサンプルを作ってみました。
まぁ、なんとなくそんな難しくないよね、と思っていたのですが、そういえばRxでマウスイベントの処理を自分で書いたことないんだよね、と、そのあたりを調べつつ試しに書いてみたらちょっと手こずりまして、助言をいただいたりもしてそれなりな感じになったのでエントリーにしておきます。

サンプルプロジェクトのダウンロードはこちら。
https://github.com/kaorun/kaorun_samples/archive/master.zip

問題のキャプチャー部分のコードはこんな感じです。
https://github.com/kaorun/kaorun_samples/blob/master/WpfScreenCaptureTest/WpfScreenCaptureTest/CaptureWindow.xaml.cs

using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Drawing;
using System.Windows.Interop;

namespace kaorun.samples
{
    /// <summary>
    /// Screen capture window class by selecting rectangle by Mouse with Rx event handling
    /// </summary>
    public partial class CaptureWindow : Window
    {
        public CaptureWindow()
        {
            InitializeComponent();
            
            Cursor = Cursors.Cross;
            var origin = new System.Windows.Point();
            
            var mouseDown = Observable.FromEventPattern<MouseEventArgs>(this, "MouseLeftButtonDown");
            var mouseMove = Observable.FromEventPattern<MouseEventArgs>(this, "MouseMove");
            var mouseUp = Observable.FromEventPattern<MouseEventArgs>(this, "MouseLeftButtonUp");

            mouseDown
                .Do(e => { origin = e.EventArgs.GetPosition(LayoutRoot); })
                .SelectMany(mouseMove)
                .TakeUntil(mouseUp)
                .Do(e =>
                {
                    var rect = BoundsRect(origin.X, origin.Y, e.EventArgs.GetPosition(LayoutRoot).X, e.EventArgs.GetPosition(LayoutRoot).Y);
                    selectionRect.Margin = new Thickness(rect.Left, rect.Top, this.Width - rect.Right, this.Height - rect.Bottom);
                    selectionRect.Width = rect.Width;
                    selectionRect.Height = rect.Height;
                })
                .LastAsync()
                .Subscribe(e =>
                {
                    this.Hide();

                    // Offsetting selection boundery, because transpalent WPF window shifted few pixel from screen coordinats
                    MainWindow.Captured = CaptureScreen(Rect.Offset(BoundsRect(origin.X, origin.Y, e.EventArgs.GetPosition(LayoutRoot).X, e.EventArgs.GetPosition(LayoutRoot).Y), this.Left, this.Top));
                    
                    Cursor = Cursors.Arrow;
                    this.Close();
                });
        }

        private static Rect BoundsRect(double left, double top, double right, double bottom)
        {
            return new Rect(Math.Min(left, right), Math.Min(top, bottom), Math.Abs(right - left), Math.Abs(bottom - top));
        }

        public static BitmapSource CaptureScreen(Rect rect)
        {
            using (var screenBmp = new Bitmap((int)rect.Width, (int)rect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
            {
                using (var bmpGraphics = Graphics.FromImage(screenBmp))
                {
                    bmpGraphics.CopyFromScreen((int)rect.X, (int)rect.Y, 0, 0, screenBmp.Size);
                    return Imaging.CreateBitmapSourceFromHBitmap(screenBmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
                }
            }
        }
    }
}

今回は、ほぼ透明な全面ウインドウを張ってマウスイベントを取得し、選択後ウインドウを隠してからスクリーンショットを撮る、というアプローチで実装してみました。
Rxの練り方はもう一声エクセレントな書き方がありそうな気がしますが、今日はこれで精いっぱい。よりエレガントなRxの書き方は随時募集中です。
Rxを使わずにイベントハンドラでやった方が黒魔術っぽくなくてサンプルとしてわかりやすい気もしますが、習作もかねているのでこんなもんで。
細かい飾りとか、画像の保存もなにもしていないし、やっつけ実装で、タスクスイッチなど異常系の処理やマルチdpi・マルチスクリーン・DirectXなどは特にケアしていないので、ちゃんと実装するならもっと頑張った方がいい気がします。
まぁ、C#でざっくり書いて68行ですかね(何がだ)。上にも書いたようにもっとケアしないといけないことが沢山あるわけですが、細かい要請に答えたり挙動を調整していけるのがネイティブアプリの肝であり醍醐味でもあります。
ところで、Windows 10ではデスクトップアプリもAppX化することでWindows Storeに登録できるようになるようです。この手のちょっとしたツールアプリでもストアで販売できるようになると思うと楽しみですね。
Special thanks emomonさん!