从鼠标位置缩放和翻译图像

Posted

技术标签:

【中文标题】从鼠标位置缩放和翻译图像【英文标题】:Zoom and translate an Image from the mouse location 【发布时间】:2020-09-07 11:11:08 【问题描述】:

问题:尝试从(或在)鼠标位置缩放(缩放)图像,使用 Paint 事件中的转换将位图原点转换为鼠标位置,然后缩放图像并将其原点转换回来。

在平移鼠标位置时,图像跳跃并且无法从重新定位的原点缩放。 旋转、缩放和平移功能正常,无需平移到鼠标位置。

在 .Net 4.7.2 上运行,在 Windows 10 1909 中使用 Visual Studio v18363.778

相关代码块:

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】:

一些建议和几个技巧。 不完全是技巧,只是在多个图形转换到位时加快计算速度的一些方法。

    分而治之:将不同的图形效果和转换拆分为不同的、专门的方法来做一件事。然后以一种使这些方法在需要时可以协同工作的方式进行设计。

    保持简单:当 Graphics 对象需要累积多个转换时,矩阵的堆叠顺序可能会导致误解。预先计算一些通用转换(主要是平移和缩放)更简单(并且不太容易产生奇怪结果),然后让 GDI+ 渲染已经预煮的对象和形状. 这里只使用了Matrix.RotateAt 和Matrix.Multiply。 这里有一些关于矩阵变换的注释:Flip the GraphicsPath

    使用正确的工具:例如,用作画布的面板并不是最佳选择。此控件不是双缓冲的;可以启用此功能,但 Panel 类不用于绘图,而 PictureBox(或非系统平面标签)本身支持它。 这里还有一些注意事项:How to apply a fade transition effect to Images

示例代码显示了 4 种缩放方法,以及生成旋转变换(并排工作,不累积)。 使用枚举器 (private enum ZoomMode) 选择缩放模式:

缩放模式

ImageLocation:图像缩放就地执行,将画布上的当前位置保持在固定位置。 CenterCanvas:在缩放图像时,它仍然位于画布的中心。 CenterMouse:图像被缩放并平移到画布上当前鼠标位置的中心。 MouseOffset:图像被缩放和平移以保持由鼠标指针在图像本身上的初始位置确定的相对位置。

您可以注意到,代码简化了所有计算,仅应用相对于定义当前图像边界的 Rectangle 且仅与此形状的位置相关的平移。 仅当计算需要预先确定鼠标滚轮生成下一个缩放因子之后图像大小将是多少时,才会缩放矩形。

已实现功能的视觉示例

示例代码

canvas 是自定义控件,派生自 PictureBox(您可以在底部找到它的定义)。此控件在代码中添加到表单,here。根据需要进行修改。 trkRotationAngle 是用于定义 Image 当前旋转的 TrackBar。将此控件添加到设计器中的表单。 radZoom_CheckedChanged 是用于设置当前缩放模式的所有 RadioButtons 的事件处理程序。这些控件设置的值在其Tag 属性中分配。将这些控件添加到设计器中的表单。
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 += canvas_MouseWheel;
        canvas.MouseMove += canvas_MouseMove;
        canvas.MouseDown += canvas_MouseDown;
        canvas.MouseUp += canvas_MouseUp;
        canvas.Paint += canvas_Paint;
        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

简单的PictureBoxEx 自定义控件(根据需要进行修改和扩展):此 PictureBox 是可选的,因此可以通过鼠标单击获得焦点

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);
        BorderStyle = BorderStyle.FixedSingle;
        Size = size;
    

【讨论】:

【参考方案2】:

@Jimi:感谢您提供详细信息 - 对于可视化图形操作中涉及的概念非常有用。我已经找到了一个有效的解决方案(参见下面的代码),但是,您的代码以更高的效率利用了步骤。诚然,我的代码是为了学习图像处理机制而开发的——因为我仍处于学习曲线的早期阶段。尽管如此,您对机制和技术的说明非常有帮助。

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);
        
    

【讨论】:

以上是关于从鼠标位置缩放和翻译图像的主要内容,如果未能解决你的问题,请参考以下文章

HTML Canvas,缩放和翻译后的鼠标位置

以鼠标位置为中心的图像缩放

用js实现页面图片,以鼠标位置为中心,滚轮缩放图片

鼠标缩放实现,以鼠标位置为中心 [算法]

在WPF里面实现以鼠标位置为中心缩放移动图片

根据当前鼠标位置缩放图形