Zoom and translate an Image from the mouse location

此生再无相见时 提交于 2020-07-07 07:21:08

问题


Issue: Attempting to zoom (scale) an Image from (or at the) mouse location using transforms in the Paint event to translate bitmap origin to mouse location, then scale the Image and translate its origin back.

  • The Image jumps and fails to scale from the relocated origin when translating the mouse location.
  • Rotate, scale, and pan function correctly without translating to the the mouse location.

Running on .Net 4.7.2, using Visual Studio in Windows 10 1909 v18363.778

The relevant code blocks:

private void trackBar1_Scroll(object sender, EventArgs e)
{
    // Get rotation angle
    ang = trackBar1.Value;
    pnl1.Invalidate();
}

private void pnl1_MouseWheel(object sender, MouseEventArgs e)
{
    // Get mouse location
    mouse = e.location;

    // Get new scale (zoom) factor
    zoom = (float)(e.Delta > 0 ? zoom * 1.05 : zoom / 1.05);
    pnl1.Invalidate();
}

private void pnl1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;
    pan = true;
    mouX = e.X;
    mouY = e.Y;
    oldX = imgX;
    oldY = imgY;
}

private void pnl1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left || !pan) return;

    // Coordinates of panned image
    imgX = oldX + e.X - mouX;
    imgY = oldY + e.Y - mouY;
    pnl1.Invalidate();
}

private void pnl1_MouseUp(object sender, MouseEventArgs e)
{
    pan = false;
}

private void pnl1_Paint(object sender, PaintEventArgs e)
{
    // Apply rotation angle @ center of bitmap
    e.Graphics.TranslateTransform(img.Width / 2, img.Height / 2);
    e.Graphics.RotateTransform(ang);
    e.Graphics.TranslateTransform(-img.Width / 2, -img.Height / 2);

    // Apply scaling factor - focused @ mouse location
    e.Graphics.TranslateTransform(mouse.X, mouse.Y, MatrixOrder.Append);
    e.Graphics.ScaleTransform(zoom, zoom, MatrixOrder.Append);
    e.Graphics.TranslateTransform(-mouse.X, -mouse.Y, MatrixOrder.Append);

    // Apply drag (pan) location
    e.Graphics.TranslateTransform(imgX, imgY, MatrixOrder.Append);

    // Draw "bmp" @ location
    e.Graphics.DrawImage(img, 0, 0);
}

回答1:


A few suggestions and a couple of tricks.
Not exactly tricks, just some methods to speed up the calculations when more than one graphic transformation is in place.

  1. Divide and conquer: split the different graphics effects and transformations in different, specialized, methods that do one thing. Then design in a way that makes it possible for this methods to work together when needed.
  2. Keep it simple: when Graphics objects need to accumulate more than a couple transformations, the order in which Matrices are stacked can cause misunderstandings. It's simpler (and less prone to generate weird outcomes) to calculate some generic transformations (translate and scale, mostly) beforehand, then let GDI+ render already pre-cooked objects and shapes.
    Here, only Matrix.RotateAt and Matrix.Multiply are used.
    Some notes about Matrix tranformations here: Flip the GraphicsPath

  3. Use the right tools: for example, a Panel used as canvas is not exactly the best choice. This Control is not double-buffered; this feature can be enabled, but the Panel not meant for drawing, while a PictureBox (or a flat Label) supports it on its own.
    Some more notes here: How to apply a fade transition effect to Images

The sample code shows 4 zoom methods, plus generates rotation transformations (which works side-by-side, don't accumulate).
The Zoom modes are selected using an enumerator (private enum ZoomMode):

Zoom modes:

  • ImageLocation: Image scaling is performed in-place, keeping the current Location on the canvas in a fixed position.
  • CenterCanvas: while the Image is scaled, it remains centered on the Canvas.
  • CenterMouse: the Image is scaled and translated to center itself on the current Mouse location on the Canvas.
  • MouseOffset: the Image is scaled and translated to maintain a relative position determined by the initial location of the Mouse pointer on the Image itself.

You can notice that the code simplifies all the calculations, applying translations exclusively relative to the Rectangle that defines the current Image bounds and only in relation to the Location of this shape.
The Rectangle is only scaled when the calculation needs to preemptively determine what the Image size will be after the Mouse Wheel has generated the next Zoom factor.

Visual sample of the implemented functionalities:

Sample code:

  • canvas is the Custom Control, derived from PictureBox (you can find its definition at the bottom). This control is added to the Form in code, here. Modify as needed.
  • trkRotationAngle is the TrackBar used to defined the current rotation of the Image. Add this control to the Form in the designer.
  • radZoom_CheckedChanged is the event handler of all the RadioButtons used to set the current Zoom Mode. The value these Controls set is assigned in their Tag property. Add these controls to the Form in the designer.

using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Windows.Forms;

public partial class frmZoomPaint : Form
{
    private float rotationAngle = 0.0f;
    private float zoomFactor = 1.0f;
    private float zoomStep = .05f;

    private RectangleF imageRect = RectangleF.Empty;
    private PointF imageLocation = PointF.Empty;
    private PointF mouseLocation = PointF.Empty;

    private Bitmap drawingImage = null;
    private PictureBoxEx canvas = null;
    private ZoomMode zoomMode = ZoomMode.ImageLocation;

    private enum ZoomMode
    {
        ImageLocation,
        CenterCanvas,
        CenterMouse,
        MouseOffset
    }

    public frmZoomPaint()
    {
        InitializeComponent();
        string imagePath = [Path of the Image];
        drawingImage = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(imagePath)));
        imageRect = new RectangleF(Point.Empty, drawingImage.Size);

        canvas = new PictureBoxEx(new Size(555, 300));
        canvas.Location = new Point(10, 10);
        canvas.MouseWheel += this.canvas_MouseWheel;
        canvas.MouseMove += this.canvas_MouseMove;
        canvas.MouseDown += this.canvas_MouseDown;
        canvas.MouseUp += this.canvas_MouseUp;
        canvas.Paint += this.canvas_Paint;
        this.Controls.Add(canvas);
    }

    private void canvas_MouseWheel(object sender, MouseEventArgs e)
    {
        mouseLocation = e.Location;
        float zoomCurrent = zoomFactor;
        zoomFactor += e.Delta > 0 ? zoomStep : -zoomStep;
        if (zoomFactor < .10f) zoomStep = .01f;
        if (zoomFactor >= .10f) zoomStep = .05f;
        if (zoomFactor < .0f) zoomFactor = zoomStep;

        switch (zoomMode) {
            case ZoomMode.CenterCanvas:
                imageRect = CenterScaledRectangleOnCanvas(imageRect, canvas.ClientRectangle);
                break;
            case ZoomMode.CenterMouse:
                imageRect = CenterScaledRectangleOnMousePosition(imageRect, e.Location);
                break;
            case ZoomMode.MouseOffset:
                imageRect = OffsetScaledRectangleOnMousePosition(imageRect, zoomCurrent, e.Location);
                break;
            default:
                break;
        }
        canvas.Invalidate();
    }

    private void canvas_MouseDown(object sender, MouseEventArgs e)
    {
        if (e.Button != MouseButtons.Left) return;
        mouseLocation = e.Location;
        imageLocation = imageRect.Location;
        canvas.Cursor = Cursors.NoMove2D;
    }

    private void canvas_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.Button != MouseButtons.Left) return;
        imageRect.Location = 
            new PointF(imageLocation.X + (e.Location.X - mouseLocation.X),
                       imageLocation.Y + (e.Location.Y - mouseLocation.Y));
        canvas.Invalidate();
    }

    private void canvas_MouseUp(object sender, MouseEventArgs e) => 
        canvas.Cursor = Cursors.Default;

    private void canvas_Paint(object sender, PaintEventArgs e)
    {
        var drawingRect = GetDrawingImageRect(imageRect);

        using (var mxRotation = new Matrix())
        using (var mxTransform = new Matrix()) {

            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;

            mxRotation.RotateAt(rotationAngle, GetDrawingImageCenterPoint(drawingRect));
            mxTransform.Multiply(mxRotation);

            e.Graphics.Transform = mxTransform;
            e.Graphics.DrawImage(drawingImage, drawingRect);
        }
    }

    private void trkRotationAngle_ValueChanged(object sender, EventArgs e)
    {
        rotationAngle = trkAngle.Value;
        canvas.Invalidate();
        canvas.Focus();
    }

    private void radZoom_CheckedChanged(object sender, EventArgs e)
    {
        var rad = sender as RadioButton;
        if (rad.Checked) {
            zoomMode = (ZoomMode)int.Parse(rad.Tag.ToString());
        }
        canvas.Focus();
    }

    #region Drawing Methods

    public RectangleF GetScaledRect(RectangleF rect, float scaleFactor) => 
        new RectangleF(rect.Location,
        new SizeF(rect.Width * scaleFactor, rect.Height * scaleFactor));

    public RectangleF GetDrawingImageRect(RectangleF rect) => 
        GetScaledRect(rect, zoomFactor);

    public PointF GetDrawingImageCenterPoint(RectangleF rect) => 
        new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);

    public RectangleF CenterScaledRectangleOnCanvas(RectangleF rect, RectangleF canvas)
    {
        var scaled = GetScaledRect(rect, zoomFactor);
        rect.Location = new PointF((canvas.Width - scaled.Width) / 2,
                                   (canvas.Height - scaled.Height) / 2);
        return rect;
    }

    public RectangleF CenterScaledRectangleOnMousePosition(RectangleF rect, PointF mousePosition)
    {
        var scaled = GetScaledRect(rect, zoomFactor);
        rect.Location = new PointF(mousePosition.X - (scaled.Width / 2),
                                   mousePosition.Y - (scaled.Height / 2));
        return rect;
    }

    public RectangleF OffsetScaledRectangleOnMousePosition(RectangleF rect, float currentZoom, PointF mousePosition)
    {
        var currentRect = GetScaledRect(imageRect, currentZoom);
        if (!currentRect.Contains(mousePosition)) return rect;

        float scaleRatio = currentRect.Width / GetScaledRect(rect, zoomFactor).Width;

        PointF mouseOffset = new PointF(mousePosition.X - rect.X, mousePosition.Y - rect.Y);
        PointF scaledOffset = new PointF(mouseOffset.X / scaleRatio, mouseOffset.Y / scaleRatio);
        PointF position = new PointF(rect.X - (scaledOffset.X - mouseOffset.X), 
                                     rect.Y - (scaledOffset.Y - mouseOffset.Y));
        rect.Location = position;
        return rect;
    }

    #endregion
}

The simple PictureBoxEx custom control (modify and extend as needed):
This PictureBox is selectable, so it can be focused, with a Mouse click

using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

[DesignerCategory("Code")]
public class PictureBoxEx : PictureBox
{
    public PictureBoxEx() : this (new Size(200, 200)){ }
    public PictureBoxEx(Size size) {
        SetStyle(ControlStyles.Selectable | ControlStyles.UserMouse, true);
        this.BorderStyle = BorderStyle.FixedSingle;
        this.Size = size;
    }
}



回答2:


@Jimi: Thank you for the detailed information - very useful for visualizing the concepts involved in the graphics manipulations. I had arrived at a functioning solution (see code below) however, your code utilizes steps with greater efficiency. Admittedly, my code is developed with more of an intent to learn the mechanics of image manipulation - as I am still at the early part of the learning curve. Nonetheless, your illustration of the mechanics and techniques is extremely helpful.

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace ZoomImage
{
    public partial class Form1 : Form
    {
        Image img;
        Bitmap bmp;
        float ang = 0;
        float zoom = 1;
        bool pan;
        bool? ctr = false;
        Point mcurrent;
        PointF mouse;
        PointF image;
        PointF _image;
        PointF rotate;

        public Form1()
        {
            InitializeComponent();
            MouseWheel += mouseWheel;

            img = Image.FromFile(@"C:\testimage.jpg");
            bmp = new Bitmap(img);

            // Set initial scale to fit canvas window
            float wRatio = (float)pbx.Width / (float)img.Width;
            float hRatio = (float)pbx.Height / (float)img.Height;
            zoom = Math.Min(wRatio, hRatio);
            image.X = (pbx.Width - zoom * img.Width) / 2;
            image.Y = (pbx.Height - zoom * img.Height) / 2;
        }

        private void label()
        {
            string _imgX = string.Format("{0:000}", image.X);
            string _imgY = string.Format("{0:000}", image.Y);
            lbl1.Text = "Location: " + _imgX + ", " + _imgY + "\r\nRotation: " + ang + "\r\nZoom: " + zoom + "\r\nMouse: " + mcurrent.X + ", " + mcurrent.Y;
        }

        private void btnRotate_Click(object sender, EventArgs e)
        {
            if (ModifierKeys == Keys.Control)
            {
                string msg = "Set center of rotation point:\r\n\nMove mouse to desired center ";
                msg += "of rotation then hold \"Alt\" and left-click.\r\n\n";
                msg += "To restore center of rotation to center of image:\r\n\nHold \"Shift\" and";
                msg += " click \"Rotate\".";
                MessageBox.Show(msg,"Change center of rotation");
                ctr = null;
                pbx.Focus();
                return;
            } 
            else if (ModifierKeys == Keys.Shift)
            {
                ctr = false;
                return;
            }
            ang = ang == 270 ? 0 : ang += 90;
            if (ang > 360) ang -= 360;
            trackBar1.Value = (int)ang;
            ctr = ctr == null ? false : ctr;
            if (ctr == false) rotate = new PointF(img.Width / 2, img.Height / 2);
            pbx.Invalidate();
        }

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            ang = trackBar1.Value;
            if (ctr == false) rotate = new PointF(img.Width / 2, img.Height / 2);
            pbx.Invalidate();
        }

        private void mouseWheel(object sender, MouseEventArgs e)
        {
            mouse = new PointF(e.X - image.X, e.Y - image.Y);

            float zinc = 0.05f;
            float zfac = 1 + zinc;
            zoom = (float)(e.Delta > 0 ? zoom * (zfac) : zoom / (zfac));

            // Adjust "img" (bitmap) orgin to maintain fixed focus @ mouse location
            if (e.Delta > 0)
            {                
                image.X -= zinc * mouse.X;
                image.Y -= zinc * mouse.Y;
            }
            else
            {
                image.X += (1 - 1 / (zfac)) * mouse.X;
                image.Y += (1 - 1 / (zfac)) * mouse.Y;
            }
            image = new PointF(image.X, image.Y);
            pbx.Invalidate();
        }

        private void mouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) return;
            if (ModifierKeys == Keys.Alt && ctr == null)
            {
                ctr = true;
                rotate = new PointF((e.X - image.X) / zoom, (e.Y - image.Y) / zoom);
                return;
            }
            pan = true;
            mouse = e.Location;
            _image = image;
        }

        private void mouseMove(object sender, MouseEventArgs e)
        {
            mcurrent = e.Location;
            label();

            if (e.Button != MouseButtons.Left || !pan) return;
            image.X = _image.X + e.X - mouse.X;
            image.Y = _image.Y + e.Y - mouse.Y;
            image = new PointF(image.X, image.Y);
            pbx.Invalidate();
        }

        private void mouseUp(object sender, MouseEventArgs e)
        {
            pan = false;
        }

        private void pbx_Paint(object sender, PaintEventArgs e)
        {
            label();

            // Generate bitmap "bmp"  - this can be saved as drawn...if deisred
            bmp = new Bitmap(img.Width, img.Height);
            using (Graphics g = Graphics.FromImage(bmp))
            {
                Matrix transform = new Matrix();
                transform.Scale(zoom, zoom, MatrixOrder.Append);
                transform.RotateAt(ang, rotate);
                transform.Translate(image.X, image.Y, MatrixOrder.Append);
                g.Transform = transform;
                g.DrawImage(img, 0, 0);
            }
            e.Graphics.DrawImage(bmp, 0, 0);
        }
    }
}


来源:https://stackoverflow.com/questions/61924051/zoom-and-translate-an-image-from-the-mouse-location

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!