【C#】塗りつぶしで、線を引いた範囲内を一色で塗りつぶす【ペイント】

Bitmapオブジェクトはピクセルごとに色を取得することができます
これを元に塗りつぶしアルゴリズムを参考にして、塗りつぶしのプログラムを追加しました

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2666090/41b01f88-893c-f6e1-8476-e3b95b74be01.gif)

```C#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;

namespace ImageEditing
{
    public partial class frmPaintTool : Form
    {
        // 変数
        Bitmap _newBitMap;              // ビットマップ画像
        bool _mouseDrug;                // マウスクリック中のフラグ
        int _prevX;                     // 前のマウスカーソルのX座標
        int _prevY;                     // 前のマウスカーソルのY座標
        int _penType;                   // ペンモード
        Color _penColor = Color.Black;  // 使用する色
        Color _fillColor;               // 塗りつぶし対象の色
        byte[] _pixels;                 // ビットマップ画像のピクセル配列
        int _stride;                    // 1行のピクセルの幅(配列を折り返す位置)
        int _pixelSize;                 // 1ピクセルの幅
        Stack<string> _pxStack = new Stack<string>();
                                        // ピクセル位置指定保存
        public frmPaintTool()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // BitMapオブジェクトを生成する
            // 背景色として白で塗りつぶす
            _newBitMap = new Bitmap(DrawingPicBox.Width, DrawingPicBox.Height);
            Graphics objGrp = Graphics.FromImage(_newBitMap);
            SolidBrush objBrush = new SolidBrush(Color.White);
            objGrp.FillRectangle(objBrush, 0, 0, _newBitMap.Width, _newBitMap.Height);
            objGrp.Dispose();
            objBrush.Dispose();
        }


        private void DrawingPicBox_MouseDown(object sender, MouseEventArgs e)
        {
            // マウスクリック開始判定
            _mouseDrug = true;

            if (_penType == 3)
            {
                // バケツ(塗りつぶし)が選択されているとき
                // StartFill()で、塗りつぶしを開始する
                StartFill(e.Location.X, e.Location.Y);
                DrawingPicBox.Image = _newBitMap;
            }

            // マウスカーソル位置記憶の初期化
            _prevX = e.Location.X;
            _prevY = e.Location.Y;
        }

        private void DrawingPicBox_MouseUp(object sender, MouseEventArgs e)
        {
            // マウスクリック終了判定
            _mouseDrug = false;
        }

        private void DrawingPicBox_MouseMove(object sender, MouseEventArgs e)
        {
            // マウスクリック中に、BitMapにGraphicsオブジェクトで描画する
            if (_mouseDrug == true)
            {
                // BitMapからGraphicsオブジェクトを生成
                // 選択されているツールの種類によって処理を分ける
                Graphics objGrp = Graphics.FromImage(_newBitMap);
                Pen objPen = null;
                Brush objBrush = null;

                if (_penType == 0)
                {
                    // ペンが選択されている場合
                    objPen = new Pen(_penColor, 3);
                    objBrush = new SolidBrush(_penColor);
                    objGrp.DrawLine(objPen, _prevX, _prevY, e.Location.X, e.Location.Y);
                    objGrp.FillEllipse(objBrush, _prevX - objPen.Width / 2, _prevY - objPen.Width / 2, objPen.Width, objPen.Width);
                    objPen.Dispose();
                }
                else if (_penType == 1)
                {
                    // 省略
                }
                else if (_penType == 2)
                {
                    // 省略
                }
                else if(_penType == 3)
                {
                    // バケツ(塗りつぶし)が選択されているとき
                    // なにもしない
                }

                // BitMapオブジェクトをPictureBoxに表示する
                objGrp.Dispose();
                _prevX = e.Location.X;
                _prevY = e.Location.Y;
                DrawingPicBox.Image = _newBitMap;
            }
        }

        private void Tools_CheckedChanged(object sender, EventArgs e)
        {
            // 現在の選択されているツールの種類を保存
            if(typePen.Checked == true)
            {
                _penType = 0;
            }
            else if(typeSquare.Checked == true)
            {
                _penType = 1;
            }
            else if (typeSelect.Checked == true)
            {
                _penType = 2;
            }
            else if (typeBucket.Checked == true)
            {
                _penType = 3;
            }
        }


        private void StartFill(int x, int y)
        {
            // 塗りつぶしの前処理
            int width = _newBitMap.Width - 1;
            int height = _newBitMap.Height - 1;
            // マウスでクリックされたピクセルの色を取得
            _fillColor = _newBitMap.GetPixel(x, y);

            if (_fillColor.R == _penColor.R && _fillColor.G == _penColor.G && _fillColor.B == _penColor.B)
            {
                // クリックされたピクの色と塗りつぶす色が一致していた場合
                // 塗りつぶしの必要なし
                return;
            }

            //塗りつぶしのためにBitmapをロックする(処理高速化)
            BitmapData bmpData = _newBitMap.LockBits(new Rectangle(0, 0, _newBitMap.Width, _newBitMap.Height), ImageLockMode.ReadWrite, _newBitMap.PixelFormat);
            IntPtr ptr = bmpData.Scan0;
            _pixels = new byte[bmpData.Stride * _newBitMap.Height];
            System.Runtime.InteropServices.Marshal.Copy(ptr, _pixels, 0, _pixels.Length);
            byte r = _penColor.R;
            byte g = _penColor.G;
            byte b = _penColor.B;
            _stride = bmpData.Stride;
            _pixelSize = 4;
            // 塗りつぶし開始
            FillLine(x, y, width, height, r, g, b);
            // 書き換えた数字を保存する
            System.Runtime.InteropServices.Marshal.Copy(_pixels, 0, ptr, _pixels.Length);
            // ロックを解除する
            _newBitMap.UnlockBits(bmpData);
        }

        private void FillLine(int x, int y, int width, int height, byte r, byte g, byte b)
        {
            // 塗りつぶしの処理
            int rightUpX = -1;
            int rightDownX = -1;
            int pos;

            while (true)
            {
                if (width <= x)
                {
                    // 画像の右端まで到達した場合
                    // (x, y)を塗る
                    pos = y * _stride + x * _pixelSize;
                    _pixels[pos] = b;
                    _pixels[pos + 1] = g;
                    _pixels[pos + 2] = r;
                }
                if (x < width && CompareColor(x + 1, y, _fillColor))
                {
                    //(x, y)より右側を行き止まり(色が違う箇所)まで行く(xをずらしていく)
                    x = x + 1;
                }
                else
                {
                    //(x, y)を塗る
                    pos = y * _stride + x * _pixelSize;
                    _pixels[pos] = b;
                    _pixels[pos + 1] = g;
                    _pixels[pos + 2] = r;
                    if (0 < y && CompareColor(x, y - 1, _fillColor))
                    {
                        rightUpX = x;
                    }
                    if (y < height && CompareColor(x, y + 1, _fillColor))
                    {
                        rightDownX = x;
                    }
                    while (true)
                    {
                        if (0 < x && CompareColor(x - 1, y, _fillColor))
                        {
                            //(x, y)より左側を塗る(xをずらしていく)
                            x = x - 1;
                            pos = y * _stride + x * _pixelSize;
                            _pixels[pos] = b;
                            _pixels[pos + 1] = g;
                            _pixels[pos + 2] = r;

                            if (0 < y && CompareColor(x, y - 1, _fillColor))
                            {
                                if (rightUpX < x)
                                {
                                    // 線の上側の塗りつぶす領域の右端を記憶する
                                    rightUpX = x;
                                }

                                if (x <= 0)
                                {
                                    // 画像の左端まで到達した場合
                                    _pxStack.Push(x + "," + (y - 1));
                                }
                                if (0 < x)
                                {
                                    if (0 < x && !CompareColor(x - 1, y - 1, _fillColor) || !CompareColor(x - 1, y, _fillColor))
                                    {
                                        // 上側の塗りつぶす領域が区切られていた場合
                                        // 塗りつぶす箇所を確定する
                                        if (rightUpX > 0)
                                        {
                                            // (rightUpX, y-1)を後で塗る
                                            _pxStack.Push(rightUpX + "," + (y - 1));
                                            rightUpX = -1;
                                        }
                                    }
                                }
                            }

                            if (y < height && CompareColor(x, y + 1, _fillColor))
                            {
                                if (rightDownX < x)
                                {
                                    // 線の下側の塗りつぶす領域の右端を記憶する
                                    rightDownX = x;
                                }

                                if (x <= 0)
                                {
                                    // 画像の左端まで到達した場合
                                    _pxStack.Push(x + "," + (y + 1));
                                }
                                if (0 < x)
                                {
                                    if (!CompareColor(x - 1, y + 1, _fillColor) || !CompareColor(x - 1, y, _fillColor))
                                    {
                                        // 下側の塗りつぶす領域が区切られていた場合
                                        // 塗りつぶす箇所を確定する
                                        if (rightDownX > 0)
                                        {
                                            // (rightDownX, y+1)を後で塗る
                                            _pxStack.Push(rightDownX + "," + (y + 1));
                                            rightDownX = -1;
                                        }
                                    }
                                }
                            }
                        }
                        else
                        {
                            //左端(色が違う箇所)まで到達
                            break;
                        }
                    }
                    break;
                }
            }

            // スタックの残数
            if (_pxStack.Count > 0)
            {
                // 塗りつぶし箇所が残っている場合
                string a = _pxStack.Pop();
                int p = Convert.ToInt32(a.Split(',')[0]);
                int q = Convert.ToInt32(a.Split(',')[1]);
                // 指定したピクセルの位置からもう一度、塗りつぶしを開始する
                FillLine(p, q, width, height, r, g, b);
            }
        }

        private bool CompareColor(int x, int y, Color color2)
        {
            // 二色を比較する処理
            int pos = y * _stride + x * _pixelSize;
            if (_pixels[pos + 2] == color2.R && _pixels[pos + 1] == color2.G && _pixels[pos] == color2.B)
            {
                // 二色が一致した場合
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}
```

画像をピクセルごとに見ていき、塗りつぶすべき色か判断しています。

まずクリックされたピクセルの右端の境界まで行き、右端のピクセルを塗りつぶす、その左隣のピクセルが塗りつぶすべき色だった場合は、左のピクセルに移り色を塗りつぶす、というのを繰り返しています。この時、塗りつぶしたピクセルの上側、下側の色も塗りつぶすべき色だった場合、あとで塗るためピクセルの位置をスタックに保存します。
左端の境界まで塗り終わったら、スタックに保存した位置のピクセルから境界まで塗りつぶすという作業をまた繰り返します。


参考にしたサイトです

https://qiita.com/Tachibana446/items/31cdda5cac78cf571a04

https://qiita.com/kob58im/items/7f3c70e8be723fc62720


線を引く処理まで

https://qiita.com/Hnming/items/fe38e3b3d2d4b677226c