未来の自分へ知識貯金

ゆーきのエンジニアブログ

約4分 C#Visual Studioプログラミング

【C#】画面の領域をマウスで範囲指定する

【C#】画面の領域をマウスで範囲指定する

Windows の Snipping Tool(Win + Shift + S)を起動すると、画面が薄暗くなってマウスで範囲を選べます。あの「画面の領域をマウスでグリッと範囲指定する」動きを、C#(Windows フォーム)で自作します。

実行すると半透明のフォームが画面いっぱいに表示され、マウスをドラッグすると選択領域が赤枠で追従し、離した時点の領域を Rectangle で取得できます。

私はこれを、画面キャプチャ&OCR ツールで「監視する領域をユーザーに指定してもらう」ために作りました。取得した Rectangle をキャプチャ処理に渡せば、Snipping Tool 風のツールがそのまま作れます。

仕組み:全画面に半透明フォームをかぶせる

やっていることはシンプルで、画面全体を覆う半透明のフォーム(オーバーレイ)を1枚かぶせて、その上でマウスイベントを拾うだけです。

  • 半透明(Opacity = 0.5)なので、下の画面が見えたまま「選択モードに入った」ことが伝わる
  • フォームが全画面を覆っているので、画面上のどこでドラッグしてもマウスイベントを取れる
  • ドラッグ中は Paint イベントで赤枠を描く

コード

■DialogRegionSelection.cs

using System;
using System.Drawing;
using System.Windows.Forms;

namespace SCSupportTool
{
    public partial class DialogRegionSelection : Form
    {
        private Point startPos;
        private Point endPos;
        private bool isSelecting;
        private int reservePaint = 0;

        public DialogRegionSelection(Screen screen)
        {
            InitializeComponent();

            this.Screen = screen;
            this.FormBorderStyle = FormBorderStyle.None;  // 枠なしで全画面を覆う
            this.BackColor = Color.LightGray;
            this.Opacity = 0.5;                           // 半透明にする
            this.StartPosition = FormStartPosition.Manual;
            this.Bounds = this.Screen.Bounds;             // 対象モニタ全体に広げる
            this.ShowInTaskbar = false;
            this.TopMost = true;
            this.MouseDown += OverlayForm_MouseDown;
            this.MouseMove += OverlayForm_MouseMove;
            this.MouseUp += OverlayForm_MouseUp;
            this.Paint += OverlayForm_Paint;
        }

        public Screen Screen { get; set; }
        public Rectangle SelectedRegion { get; set; }
        public Rectangle ScreenRegion { get { return this.RectangleToScreen(this.SelectedRegion); } }

        // ESCキーでキャンセル
        private void DialogRegionSelection_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Escape)
            {
                this.DialogResult = DialogResult.Cancel;
                this.Close();
            }
        }

        private void OverlayForm_MouseDown(object sender, MouseEventArgs e)
        {
            isSelecting = true;
            this.startPos = e.Location;
            SelectedRegion = new Rectangle(e.Location, new Size());
        }

        private void OverlayForm_MouseMove(object sender, MouseEventArgs e)
        {
            if (isSelecting)
            {
                this.endPos = e.Location;

                int x1 = this.startPos.X;
                int x2 = this.endPos.X;
                int y1 = this.startPos.Y;
                int y2 = this.endPos.Y;
                SelectedRegion = Rectangle.FromLTRB(Math.Min(x1, x2), Math.Min(y1, y2), Math.Max(x1, x2), Math.Max(y1, y2));

                this.reservePaint++;  // 再描画の予約(実際の描画はタイマーで間引く)
            }
        }

        private void OverlayForm_MouseUp(object sender, MouseEventArgs e)
        {
            isSelecting = false;
            // SelectedRegion に選択された領域が入った状態で閉じる
            this.DialogResult = DialogResult.OK;
            this.Close();
        }

        private void OverlayForm_Paint(object sender, PaintEventArgs e)
        {
            if (isSelecting)
            {
                using (var pen = new Pen(Color.Red, 2))
                {
                    e.Graphics.DrawRectangle(pen, SelectedRegion);
                }
            }
        }

        // 50ms間隔で、予約があるときだけ再描画する
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (this.reservePaint > 0)
            {
                this.reservePaint = 0;
                this.Invalidate();
            }
        }
    }
}

■DialogRegionSelection.Designer.cs

namespace SCSupportTool
{
    partial class DialogRegionSelection
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.timer1 = new System.Windows.Forms.Timer(this.components);
            this.SuspendLayout();
            // 
            // timer1
            // 
            this.timer1.Enabled = true;
            this.timer1.Interval = 50;
            this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
            // 
            // DialogRegionSelection
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(800, 450);
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
            this.KeyPreview = true;
            this.Name = "DialogRegionSelection";
            this.Text = "DialogRegionSelection";
            this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.DialogRegionSelection_KeyDown);
            this.ResumeLayout(false);
        }

        #endregion

        private System.Windows.Forms.Timer timer1;
    }
}

コードのポイント

コード量は多めですが、押さえどころは4つです。

どの方向にドラッグしても正しい矩形になる:Rectangle.FromLTRB

素朴に「開始点+ドラッグ量」で矩形を作ると、左上→右下以外の方向にドラッグしたときに幅や高さがマイナスになって破綻します。そこで Math.Min / Math.Max で「小さい方を左上、大きい方を右下」に並べ替えてから Rectangle.FromLTRB(Left, Top, Right, Bottom 指定)で矩形を作っています。これで右下→左上にドラッグしても正しく選択できます。

再描画はタイマーで間引く

MouseMove は マウスを動かすと秒間数十〜百回以上発生します。そのたびに Invalidate()(再描画)していると、画面全体を覆う半透明フォームの描画はそれなりに重いため、チラつきや CPU 負荷の原因になります。

そこで MouseMove では reservePaint++ と「描画の予約」だけをして、実際の Invalidate()50ms 間隔のタイマーが予約のあるときだけ実行しています。描画回数が最大でも秒間20回に抑えられ、見た目の追従は保ったまま負荷が下がります。

クライアント座標→スクリーン座標:RectangleToScreen

マウスイベントで取れる座標は「フォーム内の座標」です。キャプチャなどで使うのは「画面上の座標」なので、RectangleToScreen で変換したものを ScreenRegion プロパティとして公開しています。呼び出し側はこちらを使います

ESC でキャンセルできるようにする

全画面を覆うフォームは「閉じる」ボタンがないので、逃げ道として ESC キーでキャンセルできるようにしています。Designer 側で KeyPreview = true を設定しているのがポイントで、これがないとキーイベントがフォームに届かないことがあります。

使い方:マルチモニタにも対応

呼び出し側では、モニタごとにオーバーレイを1枚ずつ表示します。どのモニタでドラッグしても選択でき、1つ選択が完了したら全部閉じる、という動きです。

private List<DialogRegionSelection> dlgRegionSelectionList = new List<DialogRegionSelection>();

private void buttonSelectRegion_Click(object sender, EventArgs e)
{
    Screen[] screens = Screen.AllScreens;
    foreach (var screen in screens)
    {
        DialogRegionSelection dlg = new DialogRegionSelection(screen);
        dlg.FormClosed += RegionSelection_DialogClosed;
        dlg.Show(this);
        this.dlgRegionSelectionList.Add(dlg);
    }
}

private void RegionSelection_DialogClosed(object sender, FormClosedEventArgs e)
{
    DialogRegionSelection dlg = (DialogRegionSelection)sender;
    if (dlg.DialogResult != DialogResult.OK)
    {
        return;
    }

    Rectangle screenRegion = dlg.ScreenRegion; // 選択された領域をRectangleで受け取れる

    foreach (var d in this.dlgRegionSelectionList)
    {
        if (d.IsDisposed == false && d.Disposing == false)
        {
            d.Dispose();
        }
    }
    this.dlgRegionSelectionList.Clear();
}

ハマりポイント

  • 赤枠がチラつく → フォームの DoubleBuffered = true を設定すると描画がちらつきにくくなります。それでも気になる場合はタイマーの間隔を調整してください
  • 選択した領域とキャプチャ結果がずれる → ディスプレイの拡大率が 100% 以外の環境では、DPI 非対応アプリだと座標が読み替えられてずれます。app.manifest で DPI-Aware を宣言してください(詳細は画面キャプチャ記事のハマりポイント参照)
  • ESC が効かないKeyPreview = true が設定されているか確認してください

関連記事:組み合わせてキャプチャツールに

ここで取得した Rectangle を画面キャプチャに渡せば範囲キャプチャツールに、枠表示に渡せば「選択した領域を表示し続ける」ガイドになります。3点セットでどうぞ。

まとめ

  • 範囲選択 UI は「全画面を覆う半透明フォーム」の上でマウスイベントを拾って作る
  • 矩形は Math.Min / Math.Max + Rectangle.FromLTRB で、どの方向のドラッグにも対応
  • MouseMove のたびに再描画せず、タイマーで間引くと軽くなる
  • 呼び出し側で使うのはスクリーン座標に変換済みの ScreenRegion
  • モニタごとにオーバーレイを出せばマルチモニタ対応になる

Snipping Tool 風の UI が欲しくなったら、ぜひ流用してみてください。

この記事をシェアする

ゆーき
ゆーき

プログラマー経験10年以上。好きなプログラム言語はC#。使える言語はC++, Python他。好きな開発環境はVisual Studio。

プロフィールを見る →