【C#】ChatGPTを利用して簡単なキャプチャ機能を作ってみた

# はじめに
初めましてぽちたです。
プログラミングを初めて約1年半になります。

保守業務で設計書やプログラムを修正する際に、
あちゃこちゃと資料を並べながら作業をすることが多いのですが、
ひとつひとつの画面が小さくなってしまい、やりづらさを感じていました。

また、資料も見たい部分は一部分だけだったりするので、
「画面のスクリーンショットがずっと全面に出ていて欲しいな」と思っていました。
すでにそのようなアプリケーションが存在しますが（Raptureなど）、
今回は自分で作ってみようと思い立ちました。

今回は「ChatGPT」の力を借りながら作成してみました。
その際にいくつか問題が発生したため、解決方法についてまとめてみました。


# 要件
保存機能などは今のところ求めていないため、シンプルなものになっています。
①フォームは「キャプチャボタン」と「終了ボタン」がある。
②「キャプチャボタン」を押下すると画面の範囲を選択して、キャプチャを撮ることができる。
③範囲選択中は、選択している部分は明るく、それ以外の範囲は薄暗くなる。
④撮ったものが別ウィンドウで表示され、常に最前面にでる。
⑤別ウィンドウで出されたものは、画面サイズを変更でき、それに伴い画像のサイズも変わる。
⑥「終了ボタン」を押下すると、機能が終了する。


# ChatGPT

何回か質問を重ねまして下記のようなプログラムが作成できました。
自分でも理解するために、コメントが沢山ついています。

```chapterApp.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    using System;
    using System.Drawing;
    using System.Windows.Forms;

    public class CaptureForm : Form
    {
        //キャプチャボタン
        private Button captureButton;
        //終了ボタン
        private Button exitButton;
        //撮影した画像
        private Bitmap capturedImage;
        //キャプチャフォーム
        private ScreenCaptureForm screenCaptureForm;
        //キャプチャ画像を表示するフォーム
        private Form capturedImageForm;


        /// <summary>
        /// CaptureFormフォーム
        /// </summary>
        public CaptureForm()
        {
            Text = "CaptureForm";
            Size = new Size(300, 200);

            // キャプチャボタン
            captureButton = new Button
            {
                Text = "キャプチャ",
                Location = new Point(35, 60),
                Size = new Size(100, 40)
            };
            // キャプチャボタン押下時
            captureButton.Click += CaptureButton_Click;

            // 終了ボタン
            exitButton = new Button
            {
                Text = "終了する",
                Location = new Point(145, 60),
                Size = new Size(100, 40)
            };
            // 終了ボタン押下時
            exitButton.Click += ExitButton_Click;

            // ボタンをフォームに追加
            Controls.Add(captureButton);
            Controls.Add(exitButton);

            // フォームを作成
            screenCaptureForm = new ScreenCaptureForm();
        }

        /// <summary>
        /// キャプチャボタン押下時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CaptureButton_Click(object sender, EventArgs e)
        {
            // フォームを非表示にする
            this.Hide();

            // キャプチャ範囲を選択
            Rectangle captureArea;
            if (screenCaptureForm.ShowDialog() == DialogResult.OK)
            {
                captureArea = screenCaptureForm.CaptureRectangle;

                // スクリーン内の指定された範囲をキャプチャ
                // ビットマップを作成
                capturedImage = new Bitmap(captureArea.Width, captureArea.Height) ;
                using (Graphics g = Graphics.FromImage(capturedImage))
                {
                    // スクリーンから画像をコピー
                    g.CopyFromScreen(captureArea.Left, captureArea.Top, 0, 0, capturedImage.Size) ;
                }

                // キャプチャ画像を新しいフォームで表示
                capturedImageForm = new Form
                {
                    Text = "キャプチャ画像",
                    FormBorderStyle = FormBorderStyle.Sizable, // サイズ変更を許可
                    SizeGripStyle = SizeGripStyle.Auto, // サイズグリップを表示
                };

                // キャプチャ画像の初期表示
                PictureBox pictureBox = new PictureBox
                {
                    Image = capturedImage, // キャプチャされた画像の表示
                    Dock = DockStyle.Fill // サイズを合わせる
                };
                capturedImageForm.Controls.Add(pictureBox);

                // フォームサイズを設定
                capturedImageForm.Size = new Size(capturedImage.Width, capturedImage.Height);
                // サイズ変更イベントハンドラを設定  
                capturedImageForm.SizeChanged += CapturedImageForm_SizeChanged;

                // 最前面に表示
                capturedImageForm.TopMost = true;
                // 表示
                capturedImageForm.ShowDialog();
            }

            this.Show(); // フォームを再表示
        }

        /// <summary>
        /// 終了ボタン押下時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ExitButton_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        /// <summary>
        /// サイズ変更
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CapturedImageForm_SizeChanged(object sender, EventArgs e)
        {
            // キャプチャ画像とフォームが存在する場合
            if (capturedImage != null && capturedImageForm != null)
            {
                // キャプチャ画像の比率を保持して新しいサイズを計算
                float widthRatio = (float)capturedImage.Width / capturedImageForm.Width;
                float heightRatio = (float)capturedImage.Height / capturedImageForm.Height;
                int newWidth, newHeight;

                // 幅の比率が高い場合、幅をフォームに合わせて調整
                if (widthRatio > heightRatio)
                {
                    newWidth = capturedImageForm.Width;
                    newHeight = (int)(capturedImage.Height / widthRatio);
                }

                // 高さの比率が高い場合、高さをフォームに合わせて調整
                else
                {
                    newWidth = (int)(capturedImage.Width / heightRatio);
                    newHeight = capturedImageForm.Height;
                }

                // サイズを更新
                PictureBox pictureBox = (PictureBox)capturedImageForm.Controls[0];
                pictureBox.Image = new Bitmap(capturedImage, newWidth, newHeight);
            }
        }

        [STAThread]
        public static void Main()
        {
            Application.Run(new CaptureForm());
        }
    }

    public class ScreenCaptureForm : Form
    {
        private Point startPoint;
        private Point endPoint;
        private Rectangle captureRectangle;
        private bool isCapturing;

        /// <summary>
        /// 新規のキャプチャフォームを作成
        /// </summary>
        public Rectangle CaptureRectangle
        {
            get { return captureRectangle; }
        }

        public ScreenCaptureForm()
        {
            // 境界線なし
            FormBorderStyle = FormBorderStyle.None;
            // フォームの最大化
            WindowState = FormWindowState.Maximized;
            // 十字カーソル
            Cursor = Cursors.Cross;
            // 透明度を設定
            Opacity = 0.5;

            // ボタン押下時
            MouseDown += ScreenCaptureForm_MouseDown;
            // マウス移動時
            MouseMove += ScreenCaptureForm_MouseMove;
            // マウスが離された時
            MouseUp += ScreenCaptureForm_MouseUp;
        }

        /// <summary>
        /// ボタン押下時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ScreenCaptureForm_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                //スタートの位置を取得
                startPoint = e.Location;
                //endPoint = e.Location;
                isCapturing = true;
            }
        }

        /// <summary>
        /// マウス移動時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ScreenCaptureForm_MouseMove(object sender, MouseEventArgs e)
        {
            if (isCapturing)
            {
                // エンドの位置を取得
                endPoint = e.Location;
                captureRectangle = new Rectangle(
                    Math.Min(startPoint.X, endPoint.X),
                    Math.Min(startPoint.Y, endPoint.Y),
                    Math.Abs(endPoint.X - startPoint.X),
                    Math.Abs(endPoint.Y - startPoint.Y));
                //フォームの再描写
                Invalidate();
            }
        }

        /// <summary>
        /// マウスが離された時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ScreenCaptureForm_MouseUp(object sender, MouseEventArgs e)
        {
            if (isCapturing)
            {
                isCapturing = false;
                DialogResult = DialogResult.OK;
                Close();
            }
        }

        /// <summary>
        /// フォーム描写中の動作
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            using (SolidBrush brush = new SolidBrush(Color.FromArgb(128, Color.Black)))
            {
                Rectangle clientRect = new Rectangle(0, 0, Width, Height);

                using (Region outerRegion = new Region(clientRect))
                using (Region innerRegion = new Region(captureRectangle))
                {
                    // キャプチャ範囲を除外
                    outerRegion.Exclude(innerRegion);
                    // キャプチャ範囲の外側を描写
                    e.Graphics.FillRegion(brush, outerRegion);
                }
            }
            
            // キャプチャ範囲の境界を描写
            using (Pen pen = new Pen(Color.Red, 2))
            {
                e.Graphics.DrawRectangle(pen, captureRectangle);
            }

            // キャプチャ範囲を透明に
            using (SolidBrush transparentBrush = new SolidBrush(Color.FromArgb(0, Color.Black)))
            {
                e.Graphics.FillRectangle(transparentBrush, captureRectangle);
            }
        }
    }
}
```
# C#
動作を確認しながら、気になる部分がいくつかありました。
①画面選択時に画面がちらつく
②画面選択時に選択範囲が白く、モヤがかかったように見える
③キャプチャ後にウィンドウを閉じた後、再度キャプチャボタンを押下すると前に撮影した範囲が残って見える

### ①画面選択時に画面がちらつく
ChatGPTに「画面範囲を選択中に画面がちらつきます。解決方法を教えてください」と聞いたところ、以下のような回答が返ってきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617698/964fe0d7-9c46-e957-b6ac-1894f18db623.png)

ダブルバッファリングを知らなかったため、調べたところ下記のように書いてありました。
>ダブルバッファリングとは、画面や画像を連続的に書き換える際に、描画領域と同じサイズのバッファ領域をメモリ上に2つ用意して、交互に描画処理を行なう手法。
引用：IT用語辞典

なるほど～。
無事、ダブルバッファリングを有効にすることで、ちらつきを解決することができました。

### ②画面選択時に選択範囲が白くモヤがかかったように見える
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617698/e604597b-0b60-edde-0631-897cae451188.png)

選択している部分が少しモヤがかかったように見え、少し見えずらいです。
`ScreenCaptureForm()`にて、`Opacity`のみを利用してフォームの透明度を設定していたのですが、これが原因でした。
モヤはフォームの背景だったようです。
透明度を下げれば下げるほど、モヤ（背景）も薄くなるのですが、
つられて枠や周りの薄い黒色部分の描写も薄くなってしまいました。

```chapterApp.cs
        public ScreenCaptureForm()
        {
            // 境界線なし
            FormBorderStyle = FormBorderStyle.None;
            // フォームの最大化
            WindowState = FormWindowState.Maximized;
            // 十字カーソル
            Cursor = Cursors.Cross;
            // 透明度を設定
            Opacity = 0.5;
        }
```


フォームを透明にする方法を調べたところ、`TransparencyKey`で
透明にする領域や色を指定できるようなので使わなそうな色を背景に設定し、透明にしました。



```chapterApp.cs
        public ScreenCaptureForm()
        {
    		this.DoubleBuffered = true;
    		// 境界線なし
    		FormBorderStyle = FormBorderStyle.None;
    		// フォームの最大化
    		WindowState = FormWindowState.Maximized;
    		// 十字カーソル
    		Cursor = Cursors.Cross;
      		// 透明度を設定
    		Opacity = 0.5;
    		// 背景色を設定
    		BackColor = Color.CornflowerBlue;
    		// 背景を透明にする
    		this.TransparencyKey = Color.CornflowerBlue;
        }
```


### ③キャプチャ後にウィンドウを閉じた後、再度キャプチャボタンを押下すると、前に撮影した範囲が残って見える

下記の画像は二度目のキャプチャですが、前回選択した範囲が残ってしまっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617698/ef5e83d2-7fbb-5c6e-c069-5a515a32203a.png)

`CaptureForm()`内でフォームを作成していたことが原因でした。
「フォーム作成ボタン」押下後に、フォームを作成することで、解決することができました。

# 所感
ChatGPTってすごいですね…。
プログラムを一から自分で書くのは大変だなと感じていたので、とても助かりました！
曖昧な言葉で質問しても、解決方法をいくつか提案してくれるのも良いです。
質問内容をうまく伝えられず、検索したほうが適格な回答を得られることもあるかなと思いました。