【C#】PictureBoxに描画した線を選択する方法

Windowsフォーム上で線を選択して、あれこれしたいという話です。
線を選択する方法はいろいろありますが、今回はPictureBoxで描画した線を選択する方法について解説します。

サンプル

今回は四角の図形を結ぶ接続線を選択するというシナリオでサンプルコードを作りました。
まず、出来上がりのイメージがこちらです。

次にサンプルコードになります。
横長なのでテキストエディタにでも貼り付けて見てください。
選択の判定処理はマウスクリックイベントの部分です。

        // 図形の幅
        private const int RECT_WIDTH = 100;
        // 図形の高さ
        private const int RECT_HEIGHT = 100;
        // 接続線の配列
        private List<(int x1, int y1, int x2, int y2, string val)> LineList = new List<(int x1, int y1, int x2, int y2, string val)>();
        // 接続線の配列(選択中)
        private List<(int x1, int y1, int x2, int y2)> SelectLineList = new List<(int x1, int y1, int x2, int y2)>();

        /// <summary>
        /// フォームロード
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e)
        {
            Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            Graphics g = Graphics.FromImage(canvas);
            Pen p = new Pen(Color.Black, 1);

            // 図形1を描画
            g.DrawRectangle(p, 50, 50, RECT_WIDTH, RECT_HEIGHT); // DrawRectangle(Pen, x, y, width, height)

            // 図形2を描画
            g.DrawRectangle(p, 300, 50, RECT_WIDTH, RECT_HEIGHT); // DrawRectangle(Pen, x, y, width, height)

            // 図形3を描画
            g.DrawRectangle(p, 300, 200, RECT_WIDTH, RECT_HEIGHT); // DrawRectangle(Pen, x, y, width, height)

            // 描画する接続線の配列を定義
            LineList.Add((50 + RECT_WIDTH, 50 + (RECT_HEIGHT / 2), 300, 50 + (RECT_HEIGHT / 2), "図形1と図形2を結ぶ線"));
            LineList.Add((50 + RECT_WIDTH, 50 + (RECT_HEIGHT / 2), 300, 200 + (RECT_HEIGHT / 2), "図形1と図形3を結ぶ線"));
            foreach(var line in LineList)
            {
                // 接続線を描画
                g.DrawLine(p, line.x1, line.y1, line.x2, line.y2); // DrawLine(Pen, x1, y1, x2, y2)
            }

            //リソースを解放
            p.Dispose();
            g.Dispose();

            // PictureBoxに表示
            pictureBox1.Image = canvas;
        }

        /// <summary>
        /// マウスクリック
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
        {
            // 左クリック以外は処理を終了
            if (e.Button != MouseButtons.Left) return;

            Pen penBlack = new Pen(Color.Black, 1);
            Pen penRed = new Pen(Color.Red, 1);
            Graphics g = Graphics.FromImage(pictureBox1.Image);
            bool refreshFlg = false;
            double w;       // 接続線の幅
            double h;       // 接続線の高さ
            double r;       // 相対位置
            double targetY; // 接続線のY座標

            // 接続線の配列(選択中)を解除
            if (SelectLineList.Count > 0)
            {
                // 選択中の接続線を黒色に戻す
                g.DrawLine(penBlack, SelectLineList[0].x1, SelectLineList[0].y1, SelectLineList[0].x2, SelectLineList[0].y2); // DrawLine(Pen, x1, y1, x2, y2)

                // 配列をクリア
                SelectLineList.Clear();

                // 再描画フラグオン
                refreshFlg = true;
            }

            // 対象範囲に接続線が存在する場合
            foreach (var line in LineList)
            {
                // 接続線の幅と高さを取得
                w = line.x2 - line.x1;
                h = line.y2 - line.y1;

                // 接続線の幅からみたクリック座標の相対位置(割合)を算出
                r = (double)(e.X - line.x1) / w;

                // クリック位置.X座標にある接続線のY座標を算出
                if (line.y1 < line.y2)
                {
                    // 接続線:斜め下方向(\)の場合
                    targetY = (double)(line.y1 + (h * r));
                }else if (line.y1 > line.y2)
                {
                    // 接続線:斜め上方向(/)の場合
                    targetY = (double)(line.y2 - (h * r));
                }
                else
                {
                    // 接続線:真横(-)の場合
                    targetY = line.y1;
                }

                // クリック位置.Y座標が接続線のY座標と一致するか判定
                // (接続線のY座標前後5ピクセル分は対象とする)
                if (targetY - 5 <= e.Y && targetY + 5 >= e.Y)
                {
                    // ↓選択した場合の処理
                    // ********************

                    // コンソールに出力
                    Console.WriteLine(line.val + "をクリックしました");

                    // 接続線の配列(選択中)に追加
                    SelectLineList.Add((line.x1, line.y1, line.x2, line.y2));

                    // 対象の接続線を赤色にする
                    g.DrawLine(penRed, line.x1, line.y1, line.x2, line.y2); // DrawLine(Pen, x1, y1, x2, y2)

                    // 再描画フラグオン
                    refreshFlg = true;

                    // ********************
                    // ↑選択した場合の処理
                    break;
                }
            }

            // 再描画
            if (refreshFlg) pictureBox1.Refresh();

            //リソースを解放
            penBlack.Dispose();
            penRed.Dispose();
            g.Dispose();
        }

解説

図形の描画方法については割愛します。
接続線を選択するには、クリックした位置が選択線上にあるのか判定する必要があります。
接続線が真横に描画したパターンはX座標の開始~終了+Y座標で簡単に判定することはできますが、斜め線のパターンは少し数学的な考え方が必要です。
今回のサンプルコードではクリックしたX座標に位置する接続線のY座標を算出し、クリックしたY座標と比較することで選択有無を判定しています。

いろいろ書いていますが流れとしては
○ロードイベント
・接続線描画の際に座標情報を配列に格納
○PictureBoxのマウスクリックイベント
・接続線の配列分以下を実行
・接続線の幅と高さを算出
・幅に対するクリックしたX座標の相対位置を算出
・相対位置を元に接続線のY座標を算出
・クリックしたY座標と接続線のY座標が一致しているか判定
・一致したら選択処理を行う
になります。

X座標から相対位置を算出していますが逆にY座標から算出することもできます。
年齢のせいかこの発想に至るまで時間がかかりましたが、初めて実現できたときは我ながら感動しました。
もしかしたらもっと良い方法もあるのかもしれませんが、このサンプルは一つの考え方とし参考にしていただければ幸いです。

それでは。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です