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

标签 c# image winforms graphics gdi+

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

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

  • Running on .Net 4.7.2, using Visual Studio in Windows 10 1909 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);
    }
    

    最佳答案

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

  • 分而治之 :将不同的图形效果和转换拆分为不同的、专门的方法来做一件事。然后以一种使这些方法在需要时可以协同工作的方式进行设计。
  • 保持简单 :当 Graphics 对象需要累积多个变换时,矩阵的堆叠顺序可能会引起误解。预先计算一些通用转换(主要是平移和缩放)更简单(并且不太容易产生奇怪的结果),然后让 GDI+ 渲染已经预煮好的对象和形状。
    在这里,只有 Matrix.RotateAtMatrix.Multiply被使用。
    这里有一些关于矩阵变换的注释:Flip the GraphicsPath
  • 使用正确的工具 :例如,用作 Canvas 的 Panel 并不是最好的选择。此控件不是双缓冲的;可以启用此功能,但 Panel 类不用于绘图,而 PictureBox(或非系统平面标签)自己支持它。
    这里还有一些注意事项:How to apply a fade transition effect to Images

  • 示例代码显示了 4 种缩放方法,以及生成旋转变换(并排工作,不累积)。
    使用枚举器(private enum ZoomMode)选择缩放模式:
    缩放模式 :
  • ImageLocation :图像缩放就地执行,将 Canvas 上的当前位置保持在固定位置。
  • CenterCanvas :当图像被缩放时,它保持在 Canvas 的中心。
  • CenterMouse :图像被缩放并平移到 Canvas 上当前鼠标位置的中心。
  • MouseOffset :图像被缩放和平移以保持由鼠标指针在图像本身上的初始位置确定的相对位置。

  • 您会注意到,代码简化了所有计算,仅应用相对于定义当前图像边界的 Rectangle 且仅与此形状的位置相关的平移。
    Rectangle 仅在计算需要预先确定 Image 大小为 时才缩放。之后 鼠标滚轮已生成下一个缩放因子。
    已实现功能的视觉示例 :
    GDI+ Zoom and Rotations Samples
    示例代码 :
  • canvas 是自定义控件,派生自 PictureBox(您可以在底部找到它的定义)。此控件在代码中添加到表单,here。根据需要进行修改。
  • trkRotationAngle 是用来定义Image当前旋转的TrackBar。将此控件添加到设计器中的窗体。
  • radZoom_CheckedChanged 是用于设置当前缩放模式的所有 RadioButtons 的事件处理程序。这些控件设置的值在它们的Tag 中分配。属性(property)。将这些控件添加到设计器中的窗体。
  • 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;
        }
    }
    

    关于c# - 从鼠标位置缩放和翻译图像,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61924051/

    相关文章:

    c++ - 如何使用 DLIB 库加载 jpeg 文件?

    c# - 保存和还原DataTable的最佳方法C#

    .net - 使用 CodeDom 编写 VB.NET Windows 窗体应用程序

    C# 获取枚举值

    c# - 如何在单击按钮时检查 gridview 列中复选框的状态

    c# - extern 和 static 的顺序重要吗?

    winforms - 形成 Dispose() 或 Close()

    c# - 执行函数 : MyFunction -> Unable to find assembly 'MySolution. MyProject.MyService 时出现 Azure Redis 异常

    使用相机单击照片后,iPhone将照片存储在应用程序中

    c++ - 计算像素坐标 x 和 y