Search code examples
c#winformsbitmappicturebox

How to draw on a zoomed image?


I have a picturebox of Size 400X400 in my application. The SizeMode of the picturebox is set to Zoomed. I have loaded a png image of 700X446 in the picturebox.

enter image description here

I have following issue,

Even though I am drawing a black straight line along the red path, actually it is being shown along the loaded image out of perspective.

How can I solve the issue?

P.S. I want to draw only on the image, not the entire picturebox.

Source Code:

 public partial class MainForm : Form
 {
    Bitmap _inputImage = null;
    //Graphics _imageGraphics = null;

    #region ctor
    public MainForm()
    {
        InitializeComponent();

        _inputImage = Bitmap.FromFile(@"E:\cracked.png") as Bitmap;

        this.inputImagePictureBox.Image = _inputImage;
    }
    #endregion

    #region Mouse Up and Down
    Point _startPoint = Point.Empty;

    private void left_MouseDown(object sender, MouseEventArgs e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            _startPoint = e.Location;

            Circle tempCircle = new Circle(_startPoint, 10);

            Bitmap tempImage = (Bitmap)_inputImage.Clone();

            Graphics g = Graphics.FromImage(tempImage);

            tempCircle.Draw(g);

            inputImagePictureBox.Image = tempImage;
        }
    }

    private void pressed_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            if (_startPoint != e.Location)
            {
                Line tempLine = new Line(_startPoint, e.Location);

                Bitmap tempImage = (Bitmap)_inputImage.Clone();

                Graphics g = Graphics.FromImage(tempImage);

                tempLine.Draw(g);

                inputImagePictureBox.Image = tempImage;
            }
        }
    }

    Bitmap _savedImage;

    private void left__MouseUp(object sender, MouseEventArgs e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            if (_startPoint != e.Location)
            {
                Line tempLine = new Line(_startPoint, e.Location);

                Bitmap tempImage = (Bitmap)_inputImage.Clone();

                Graphics g = Graphics.FromImage(tempImage);

                tempLine.Draw(g);

                _savedImage = tempImage;

                inputImagePictureBox.Image = tempImage;
            }
            else
            {
                Bitmap tempImage = (Bitmap)_inputImage.Clone();
                Graphics g = Graphics.FromImage(tempImage);

                inputImagePictureBox.Image = tempImage;
            }
        }
    } 
}

Solution

  • You need to address two issues:

    • Clip the Graphics area to the actual Image instead of the whole PictureBox.ClientArea

    • Scale the coordinates of the mouse events to the actual image when receiving and recording them and back again when you use them to draw in the Paint event.

    For both we need to know the zoom factor of the Image; for the clipping we also need to know the ImageArea and for drawing I simply store two mouse locations.

    Here are the class level variable I use:

    PointF mDown = Point.Empty;
    PointF mLast = Point.Empty;
    float zoom = 1f;
    RectangleF ImgArea = RectangleF.Empty;
    

    Note that I use floats for all, since we will need to do some dividing..

    First we'll calculate the zoom and the ImageArea:

    void GetImageScaleData(PictureBox pbox)
    {
        SizeF sp = pbox.ClientSize;
        SizeF si = pbox.Image.Size;
        float rp = 1f * sp.Width / sp.Height;   // calculate the ratios of
        float ri = 1f * si.Width / si.Height;   // pbox and image
    
        if (rp > ri)
        {
            zoom = sp.Height / si.Height;
            float width = si.Width * zoom;
            float left = (sp.Width - width) / 2;
            ImgArea = new RectangleF(left, 0, width, sp.Height);
        }
        else
        {
            zoom = sp.Width / si.Width;
            float height = si.Height * zoom;
            float top = (sp.Height - height) / 2;
            ImgArea = new RectangleF(0, top, sp.Width, height);
        }
    }
    

    This routine should be called each time a new Image is loaded and also upon any Resizing of the PictureBox:

    private void pictureBox1_Resize(object sender, EventArgs e)
    {
        GetImageScaleData(pictureBox1);
    }
    

    Now ne need store the mouse locations. Since they must be reusable after a resize we need to tranfsorm them to image coordinates. This routine can do that and also back again:

    PointF scalePoint(PointF pt, bool scale)
    {
        return scale ? new PointF( (pt.X - ImgArea.X) / zoom, (pt.Y - ImgArea.Y) / zoom)
                     : new PointF( pt.X * zoom + ImgArea.X, pt.Y * zoom + ImgArea.Y);
    }
    

    Finally we can code the Paint event

    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        using (Pen pen = new Pen(Color.Fuchsia, 2.5f) { DashStyle = DashStyle.Dot})
            e.Graphics.DrawRectangle(pen, Rectangle.Round(ImgArea));
    
        e.Graphics.SetClip(ImgArea);
        e.Graphics.DrawLine(Pens.Red, scalePoint(mDown, false), scalePoint(mLast, false));
    }
    

    .. and the mouse events:

    private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
    {
        mDown = scalePoint(e.Location, true);
    }
    
    private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            mLast = scalePoint(e.Location, true);
            pictureBox1.Invalidate();
        }
    }
    

    enter image description here

    For more complex drawing you would store the coordinates in List<PointF> and transform them back, pretty much like above..:

    List<PointF> points = new List<PointF>();
    

    and then:

    e.Graphics.DrawCurve(Pens.Orange, points.Select(x => scalePoint(x, false)).ToArray());