【C#】画像を拡大・縮小してマウスで線を引く【ペイント】

途中で画像を拡大・縮小してもなるべく違和感がないようにしています。
拡大ボタンを押すと、倍率が2倍、4倍、8倍・・・と増えていきます。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2666090/7285913e-d917-db34-1583-441c0487221b.gif)

```C#
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;

namespace ImageEditing
{
    public partial class frmPaintTool : Form
    {
        // 変数
        Bitmap _newBitMap;              //ビットマップ画像
        Bitmap _scaledBitMap;           //拡大・縮小されたビットマップ画像
        bool _mouseDrug;                //マウスクリック中のフラグ
        int _prevX;                     //前のマウスカーソルのX座標
        int _prevY;                     //前のマウスカーソルのY座標
        int _magnification = 3;         //現在のサイズ倍率段階(1倍)
        int DEFAULT_SIZE = 3;           //通常のサイズ倍率段階(1倍)
        int MAX_SIZE = 6;               //最大拡大率段階
        int MIN_SIZE = 0;               //最小縮小率段階

        public frmPaintTool()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //PictureBoxのサイズに合わせて、BitMapオブジェクトを生成する
            _newBitMap = new Bitmap(DrawingPicBox.Width, DrawingPicBox.Height);
        }


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

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

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

            if(_magnification != DEFAULT_SIZE)
            {
                // _newBitMapを拡大・縮小したビットマップ画像を表示する
                DisplayScaledBitmap();
            }
        }

        private void DrawingPicBox_MouseMove(object sender, MouseEventArgs e)
        {
            // マウスクリック中に、BitMapにGraphicsオブジェクトで描画する
            if (_mouseDrug == true)
            {
                // BitMapからGraphicsオブジェクトを生成
                // 描画に利用するペンの色、太さを設定
                // 指定したペンでマウスの位置に線を引く
                Graphics objGrp = Graphics.FromImage(_newBitMap);
                Pen objPen = new Pen(Color.Black, 3);      
                
                if(_magnification == DEFAULT_SIZE)
                {
                    // _newBitMapに線を引く
                    objGrp.DrawLine(objPen, _prevX, _prevY, e.Location.X, e.Location.Y);
                    _prevX = e.Location.X;
                    _prevY = e.Location.Y;
                    objPen.Dispose();
                }
                else
                {
                    // _newBitMapに線を引き、線の太さを倍率に合わせて変更する
                    if (_magnification >= DEFAULT_SIZE)
                    {
                        int scale = (int)Math.Pow(2, _magnification - DEFAULT_SIZE);
                        objGrp.DrawLine(objPen, _prevX / scale, _prevY / scale, e.Location.X / scale, e.Location.Y / scale);
                        objPen = new Pen(Color.Black, 3 * (int)Math.Pow(2, _magnification - 3));
                    }
                    else
                    {
                        int scale = (int)Math.Pow(2, DEFAULT_SIZE - _magnification);
                        objGrp.DrawLine(objPen, _prevX * scale, _prevY * scale, e.Location.X * scale, e.Location.Y * scale);
                        objPen = new Pen(Color.Black, 3 / (int)Math.Pow(2, 3 - _magnification));
                    }

                    // _scaledBitMapに線を引く
                    objGrp = Graphics.FromImage(_scaledBitMap);
                    objGrp.DrawLine(objPen, _prevX, _prevY, e.Location.X, e.Location.Y);
                    _prevX = e.Location.X;
                    _prevY = e.Location.Y;
                    objPen.Dispose();
                }

                // BitMapオブジェクトをPictureBoxに表示する
                objGrp.Dispose();
                if (_magnification == DEFAULT_SIZE)
                {
                    // _newBitMapをPictureBoxに表示する
                    DrawingPicBox.Image = _newBitMap;
                }
                else
                {
                    // _scaledBitMapをPictureBoxに表示する
                    DrawingPicBox.Image = _scaledBitMap;
                }     
            }
        }

        private void btnExpand_Click(object sender, EventArgs e)
        {
            //「拡大」ボタンが押されたとき
            _magnification = _magnification + 1;
            if (_magnification > MAX_SIZE)
            {
                _magnification = MAX_SIZE;
            }

            //拡大・縮小された画像を表示する
            DisplayScaledBitmap();
        }

        private void btnShrink_Click(object sender, EventArgs e)
        {
            //「縮小」ボタンが押されたとき
            _magnification = _magnification - 1;
            if (_magnification < MIN_SIZE)
            {
                _magnification = MIN_SIZE;
            }

            //拡大・縮小された画像を表示する
            DisplayScaledBitmap();
        }


        private void DisplayScaledBitmap()
        {
            // 拡大・縮小されたビットマップ画像をPictureBoxに表示する処理
            _scaledBitMap = (Bitmap)_newBitMap.Clone();
            if (_magnification == DEFAULT_SIZE)
            {
                DrawingPicBox.Width = _newBitMap.Width;
                DrawingPicBox.Height = _newBitMap.Height;
                DrawingPicBox.Image = _newBitMap;
            }
            else
            {
                BitmapSizeChange();
                DrawingPicBox.Image = _scaledBitMap;
            }
        }

        private void BitmapSizeChange()
        {
            // サイズ変更したビットマップ(_scaledBitMap)を作成する処理
            if (_magnification >= DEFAULT_SIZE)
            {
                // 画像を拡大する場合
                int scale = (int)Math.Pow(2, _magnification - DEFAULT_SIZE);             //拡大率取得
                _scaledBitMap = ExpandBitmap(_scaledBitMap, _scaledBitMap.Width * scale, _scaledBitMap.Height * scale, InterpolationMode.NearestNeighbor);
            }
            else
            {
                // 画像を縮小する場合
                int scale = (int)Math.Pow(2, DEFAULT_SIZE - _magnification);             //縮小率取得
                int w = _scaledBitMap.Width / scale;
                int h = _scaledBitMap.Height / scale;
                DrawingPicBox.Width = w;
                DrawingPicBox.Height = h;
                Bitmap dst = new Bitmap(w, h);
                Graphics g = Graphics.FromImage(dst);
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.DrawImage(_scaledBitMap, 0, 0, w, h);
                g.Dispose();
                _scaledBitMap = (Bitmap)dst.Clone();
                dst.Dispose();
            }
        }

        private Bitmap ExpandBitmap(Bitmap original, int width, int height, InterpolationMode interpolationMode)
        {
            // 画像を指定されたサイズにまで拡大する処理
            Bitmap bmpResize;
            Bitmap bmpResizeColor;
            Graphics graphics = null;
            try
            {
                PixelFormat pf = original.PixelFormat;
                DrawingPicBox.Width = width;
                DrawingPicBox.Height = height;
                bmpResizeColor = new Bitmap(width, height, pf);
                var dstRect = new RectangleF(0, 0, width, height);
                var srcRect = new RectangleF(-0.5f, -0.5f, original.Width, original.Height);
                graphics = Graphics.FromImage(bmpResizeColor);
                graphics.Clear(Color.Transparent);
                graphics.InterpolationMode = interpolationMode;
                graphics.DrawImage(original, dstRect, srcRect, GraphicsUnit.Pixel);
            }
            finally
            {
                if (graphics != null)
                {
                    graphics.Dispose();
                }
            }
            bmpResize = bmpResizeColor;
            return bmpResize;
        }
    }
}
```

Bitmapオブジェクトのサイズ(横幅、縦幅)が増えるほど処理がどんどん重くなっていくので、最大8倍(800%)までで止めています。
基本的に縮小した画像をそのまま元のサイズに戻すと線の形がぼやけてしまうことが多いので、表示するのは拡大・縮小された画像(その度に描画する)、実際に線を引くのは通常サイズの画像(常に持っておく)と分けています。

参考にしたサイトです

https://imagingsolution.net/program/csharp/rezise_bitmap_data/

https://dobon.net/vb/dotnet/graphics/interpolationmode.html

線を引く処理

https://qiita.com/0mmm/items/fe38e3b3d2d4b677226c