从鼠标位置缩放和平移图像

gal*_*leo 4 c# graphics gdi+ image winforms

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

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

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

相关代码块:

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);
}
Run Code Online (Sandbox Code Playgroud)

Jim*_*imi 5

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

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

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

  3. 使用正确的工具:例如,用作画布的面板并不是最佳选择。此控件不是双缓冲的;可以启用此功能,但 Panel 类不适用于绘图,而 PictureBox(或非系统平面标签)本身支持它。
    这里还有一些注意事项:如何将淡入淡出过渡效果应用于图像

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

缩放模式

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

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

已实现功能的可视化示例

GDI+ 缩放和旋转示例

示例代码

  • canvas是自定义控件,派生自 PictureBox(您可以在底部找到它的定义)。此控件以代码形式添加到表单中,请点击此处。根据需要进行修改。
  • trkRotationAngle是用于定义图像当前旋转的 TrackBar。将此控件添加到设计器中的表单。
  • radZoom_CheckedChanged是用于设置当前缩放模式的所有 RadioButton 的事件处理程序。这些控件设置的值在它们的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 += 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
}
Run Code Online (Sandbox Code Playgroud)

简单的PictureBoxEx自定义控件(根据需要修改和扩展):
此图片框是可选的,因此可以通过鼠标单击来聚焦

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;
    }
}
Run Code Online (Sandbox Code Playgroud)