C#
WPF
Rx

Gyazoライクな画面範囲選択をC#とReactive Extensions (Rx)で書いた

More than 3 years have passed since last update.

どうもGyazoのような画面範囲選択について「このUIをWindowsで実装しようとすると、千行近いコードをCか何かで書かなければなら」ないらしいですが、雑に作ったScreenCaptureWrapper初版(→画面を動画キャプチャするツールScreenCaptureWrapperを公開)でもとてもそんな長さにならなかったよなぁと思いつつも、Rxを使うとさらにすっきり書けそうだったので、書いてみました。

public static Task<Rect> SelectScreenPositionAsync()
{
    var shapeRect = new Rectangle() { Fill = new SolidColorBrush(Color.FromArgb(0x44, 0x99, 0, 0)), Stroke = Brushes.Red };
    var canvas = new Canvas();
    canvas.Children.Add(shapeRect);

    var window = new Window()
    {
        Title = "Select Screen Position", Top = 0, Left = 0,
        Width = SystemParameters.VirtualScreenWidth, Height = SystemParameters.VirtualScreenHeight,
        WindowStyle = WindowStyle.None, Topmost = true, ShowInTaskbar = false,
        AllowsTransparency = true,
        Background = new SolidColorBrush(Color.FromArgb(1, 0xff, 0xff, 0xff)),
        Cursor = Cursors.Cross, Content = canvas
    };

    var rectObservable = Observable.FromEventPattern<MouseEventArgs>(window, "MouseLeftButtonDown")
        .Select(ev => ev.EventArgs.GetPosition(window))
        .CombineLatest(Observable.FromEventPattern<MouseEventArgs>(window, "MouseMove"),
           (downPos, ev) =>
           {
               var movePos = ev.EventArgs.GetPosition(window);
               return new Rect(Math.Min(downPos.X, movePos.X), Math.Min(downPos.Y, movePos.Y),
                               Math.Abs(downPos.X - movePos.X), Math.Abs(downPos.Y - movePos.Y));
           })
        .TakeUntil(Observable.FromEventPattern<MouseEventArgs>(window, "MouseLeftButtonUp"));

    rectObservable.Subscribe(r =>
    {
        Canvas.SetLeft(shapeRect, r.X);
        Canvas.SetTop(shapeRect, r.Y);
        shapeRect.Width = r.Width;
        shapeRect.Height = r.Height;
    }, window.Close);

    window.Show();

    return rectObservable.ToTask(); // return a last value
}

本当は最初のほうはXAMLで書いてあげたほうがいろいろいじりやすくて便利だとは思いますが、そこはお好みでどうぞ。

(追記)
なお、HTML+JSがダメだと言っているわけではないです。ElectronはブラウザのレンダラプロセスのメッセージループにNode(io.js)を統合するとかcrazyで面白いので、それはそれで使っていきたいと思っています。

元アプリはReactで書かれていて、これはこれでまぁ良いんじゃないかと思いますが、このウィンドウであれば、以下のあたりのようなRx系ライブラリで、イベントからのDOMへ反映という形にすると、それはそれですっきりしそうだなぁという気がします。なお、私は(今のところ)Bacon.jsが好きです。