简体   繁体   English

C# winforms - 如何结合滚动条和鼠标滚轮缩放?

[英]C# winforms - How to combine scrollbar with mouse wheel zooming?

Problem - I'm writing a program that draws graphics, and zooming is one of the features.问题- 我正在编写一个绘制图形的程序,缩放是其中一个功能。 Currently, a picturebox is placed on a panel , and the picturebox has vertical and horizontal scroll bars on the right and bottom.目前,一个picturebox放置在一个panel上,图片框的右侧和picturebox都有垂直和水平滚动条。 How to combine scrollbar with mouse wheel zooming?如何将滚动条与鼠标滚轮缩放结合起来? And I'm not sure if I should use paint to draw the graphics or set a bitmap to draw the graphics onto it?而且我不确定是否应该使用paint来绘制图形或设置 bitmap 来在其上绘制图形?

Expected - When the mouse wheel is scrolled, the entire canvas( picturebox ) include drawn graphics are scaled according to the current mouse position as the center (the horizontal and vertical scroll bars change according to the zoom center).预期- 当鼠标滚轮滚动时,整个画布( picturebox )包括绘制的图形以当前鼠标 position 为中心进行缩放(水平和垂直滚动条根据缩放中心变化)。 When the mouse wheel is pressed and moved, the canvas can be dragged freely.当鼠标滚轮按下移动时,canvas可以自由拖动。

Expected as follows:预期如下: 在此处输入图像描述

The initial code初始代码

private List<Point> _points;
private int _pointRadius = 50;
private float _scale = 1f;
private float _offsetX = 0f;
private float _offsetY = 0f;

private void picturebox_MouseDown(object sender, MouseEventArgs e)
{
    _points.Add(e.Location);
}

private void picturebox_MouseWheel(object sender, MouseEvnetArgs e)
{
    if(e.Delta < 0)
    {
        _scale += 0.1f;
        _offsetX = e.X * (1f - _scale);
        _offsetY = e.X * (1f - _scale);
    }
    else
    {
        _scale -= 0.1f;
        _offsetX = e.X * (1f - _scale);
        _offsetY = e.X * (1f - _scale);
    }
    picturebox.Invalidate();
}

private void picturebox_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.TranslateTransform(_offsetX, _offsetY);
    e.Graphics.ScaleTransform(_scaleX, _scaleY);
    foreach (Point p in _points)
    {
        e.Graphics.FillEllipse(Brushes.Black, p.X, - _pointRadius, p.Y - _pointRadius, 2 * _pointRadius, 2 * _pointRadius);
    }
}

Hope the answer is modified based on the initial code.希望根据初始代码修改答案。

Thanks in advance to everyone who helped me.提前感谢所有帮助过我的人。

Would it be easier if I drew the graphics on a Bitmap?如果我在 Bitmap 上绘制图形会更容易吗?

Considering the nature of your task and the already implemented solutions in my ImageViewer I created a solution that draws the result in a Metafile , which is both elegant, consumes minimal memory and allows zooming without quality issues.考虑到您的任务的性质和我的ImageViewer中已经实现的解决方案,我创建了一个解决方案,该解决方案将结果绘制到Metafile中,它既优雅,又消耗最少的 memory 并允许在没有质量问题的情况下进行缩放。

Here is the stripped version of my ImageViewer:这是我的ImageViewer:

public class MetafileViewer : Control
{
    private HScrollBar sbHorizontal = new HScrollBar { Visible = false };
    private VScrollBar sbVertical = new VScrollBar { Visible = false };
    private Metafile? image;
    private Size imageSize;
    private Rectangle targetRectangle;
    private Rectangle clientRectangle;
    private float zoom = 1;
    private bool sbHorizontalVisible;
    private bool sbVerticalVisible;
    private int scrollFractionVertical;

    public MetafileViewer()
    {
        Controls.AddRange(new Control[] { sbHorizontal, sbVertical });
        sbHorizontal.ValueChanged += ScrollbarValueChanged;
        sbVertical.ValueChanged += ScrollbarValueChanged;
    }

    void ScrollbarValueChanged(object? sender, EventArgs e) => Invalidate();

    public Metafile? Image
    {
        get => image;
        set
        {
            image = value;
            imageSize = image?.Size ?? default;
            InvalidateLayout();
        }
    }

    public bool TryTranslate(Point mouseCoord, out PointF canvasCoord)
    {
        canvasCoord = default;
        if (!targetRectangle.Contains(mouseCoord))
            return false;
        canvasCoord = new PointF((mouseCoord.X - targetRectangle.X) / zoom, (mouseCoord.Y - targetRectangle.Y) / zoom);
        if (sbHorizontalVisible)
            canvasCoord.X += sbHorizontal.Value / zoom;
        if (sbVerticalVisible)
            canvasCoord.Y += sbVertical.Value / zoom;

        return true;
    }

    private void InvalidateLayout()
    {
        Invalidate();
        if (imageSize.IsEmpty)
        {
            sbHorizontal.Visible = sbVertical.Visible = sbHorizontalVisible = sbVerticalVisible = false;
            targetRectangle = Rectangle.Empty;
            return;
        }

        Size clientSize = ClientSize;
        if (clientSize.Width < 1 || clientSize.Height < 1)
        {
            targetRectangle = Rectangle.Empty;
            return;
        }

        Size scaledSize = imageSize.Scale(zoom);

        // scrollbars visibility
        sbHorizontalVisible = scaledSize.Width > clientSize.Width
            || scaledSize.Width > clientSize.Width - SystemInformation.VerticalScrollBarWidth && scaledSize.Height > clientSize.Height;
        sbVerticalVisible = scaledSize.Height > clientSize.Height
            || scaledSize.Height > clientSize.Height - SystemInformation.HorizontalScrollBarHeight && scaledSize.Width > clientSize.Width;

        if (sbHorizontalVisible)
            clientSize.Height -= SystemInformation.HorizontalScrollBarHeight;
        if (sbVerticalVisible)
            clientSize.Width -= SystemInformation.VerticalScrollBarWidth;
        if (clientSize.Width < 1 || clientSize.Height < 1)
        {
            targetRectangle = Rectangle.Empty;
            return;
        }

        Point clientLocation = Point.Empty;
        var targetLocation = new Point((clientSize.Width >> 1) - (scaledSize.Width >> 1),
            (clientSize.Height >> 1) - (scaledSize.Height >> 1));

        // both scrollbars
        if (sbHorizontalVisible && sbVerticalVisible)
        {
            sbHorizontal.Dock = sbVertical.Dock = DockStyle.None;
            sbHorizontal.Width = clientSize.Width;
            sbHorizontal.Top = clientSize.Height;
            sbHorizontal.Left = 0;
            sbVertical.Height = clientSize.Height;
            sbVertical.Left = clientSize.Width;
        }
        // horizontal scrollbar
        else if (sbHorizontalVisible)
            sbHorizontal.Dock = DockStyle.Bottom;
        // vertical scrollbar
        else if (sbVerticalVisible)
            sbVertical.Dock = DockStyle.Right;

        // adjust scrollbar values
        if (sbHorizontalVisible)
        {
            sbHorizontal.Minimum = targetLocation.X;
            sbHorizontal.Maximum = targetLocation.X + scaledSize.Width;
            sbHorizontal.LargeChange = clientSize.Width;
            sbHorizontal.SmallChange = 32;
            sbHorizontal.Value = Math.Min(sbHorizontal.Value, sbHorizontal.Maximum - sbHorizontal.LargeChange);
        }

        if (sbVerticalVisible)
        {
            sbVertical.Minimum = targetLocation.Y;
            sbVertical.Maximum = targetLocation.Y + scaledSize.Height;
            sbVertical.LargeChange = clientSize.Height;
            sbVertical.SmallChange = 32;
            sbVertical.Value = Math.Min(sbVertical.Value, sbVertical.Maximum - sbVertical.LargeChange);
        }

        sbHorizontal.Visible = sbHorizontalVisible;
        sbVertical.Visible = sbVerticalVisible;

        clientRectangle = new Rectangle(clientLocation, clientSize);
        targetRectangle = new Rectangle(targetLocation, scaledSize);
        if (sbVerticalVisible)
            clientRectangle.X = SystemInformation.VerticalScrollBarWidth;
    }

    protected override void OnSizeChanged(EventArgs e)
    {
        base.OnSizeChanged(e);
        InvalidateLayout();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        if (image == null || e.ClipRectangle.Width <= 0 || e.ClipRectangle.Height <= 0)
            return;

        if (targetRectangle.IsEmpty)
            InvalidateLayout();
        if (targetRectangle.IsEmpty)
            return;

        Graphics g = e.Graphics;
        g.IntersectClip(clientRectangle);
        Rectangle dest = targetRectangle;
        if (sbHorizontalVisible)
            dest.X -= sbHorizontal.Value;
        if (sbVerticalVisible)
            dest.Y -= sbVertical.Value;
        g.DrawImage(image, dest);
        g.DrawRectangle(SystemPens.ControlText, Rectangle.Inflate(targetRectangle, 1, 1));
    }

    protected override void OnMouseWheel(MouseEventArgs e)
    {
        base.OnMouseWheel(e);
        switch (ModifierKeys)
        {
            // zoom
            case Keys.Control:
                float delta = (float)e.Delta / SystemInformation.MouseWheelScrollDelta / 5;
                if (delta.Equals(0f))
                    return;
                delta += 1;
                SetZoom(zoom * delta);
                break;

            // vertical scroll
            case Keys.None:
                VerticalScroll(e.Delta);
                break;
        }
    }

    private void VerticalScroll(int delta)
    {
        // When scrolling by mouse, delta is always +-120 so this will be a small change on the scrollbar.
        // But we collect the fractional changes caused by the touchpad scrolling so it will not be lost either.
        int totalDelta = scrollFractionVertical + delta * sbVertical.SmallChange;
        scrollFractionVertical = totalDelta % SystemInformation.MouseWheelScrollDelta;
        int newValue = sbVertical.Value - totalDelta / SystemInformation.MouseWheelScrollDelta;
        SetValueSafe(sbVertical, newValue);
    }

    internal static void SetValueSafe(ScrollBar scrollBar, int value)
    {
        if (value < scrollBar.Minimum)
            value = scrollBar.Minimum;
        else if (value > scrollBar.Maximum - scrollBar.LargeChange + 1)
            value = scrollBar.Maximum - scrollBar.LargeChange + 1;

        scrollBar.Value = value;
    }

    private void SetZoom(float value)
    {
        const float maxZoom = 10f;
        float minZoom = image == null ? 1f : 1f / Math.Min(imageSize.Width, imageSize.Height);
        if (value < minZoom)
            value = minZoom;

        if (value > maxZoom)
            value = maxZoom;

        if (zoom.Equals(value))
            return;

        zoom = value;
        InvalidateLayout();
    }
}

And then the updated version of your initial code (add a new point by right click, zoom by Ctrl + mouse scroll):然后是初始代码的更新版本(通过右键单击添加新点,通过 Ctrl + 鼠标滚动进行缩放):

public partial class RenderMetafileForm : Form
{
    private static Size canvasSize = new Size(300, 200);
    private List<PointF> points = new List<PointF>();
    private const float pointRadius = 5;

    public RenderMetafileForm()
    {
        InitializeComponent();
        metafileViewer.MouseClick += MetafileViewer_MouseClick;
        UpdateMetafile();
    }

    private void MetafileViewer_MouseClick(object? sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Right && metafileViewer.TryTranslate(e.Location, out var coord))
        {
            points.Add(coord);
            UpdateMetafile();
        }
    }

    private void UpdateMetafile()
    {
        Graphics refGraph = Graphics.FromHwnd(IntPtr.Zero);
        IntPtr hdc = refGraph.GetHdc();
        Metafile result;
        try
        {
            result = new Metafile(hdc, new Rectangle(Point.Empty, canvasSize), MetafileFrameUnit.Pixel, EmfType.EmfOnly, "Canvas");
            using (var g = Graphics.FromImage(result))
            {
                foreach (PointF point in points)
                    g.FillEllipse(Brushes.Navy, point.X - pointRadius, point.Y - pointRadius, pointRadius * 2, pointRadius * 2);
            }
        }
        finally
        {
            refGraph.ReleaseHdc(hdc);
            refGraph.Dispose();
        }

        Metafile? previous = metafileViewer.Image;
        metafileViewer.Image = result;
        previous?.Dispose();
    }
}

Result:结果:

带缩放的元文件渲染

⚠️ Note: I did not add panning by keyboard or by grabbing the image but you can extract those from the original ImageViewer . ⚠️注意:我没有通过键盘或抓取图像添加平移,但您可以从原始ImageViewer中提取它们。 Also, I removed DPI-aware scaling but see the ScaleSize extensions in the linked project.此外,我删除了 DPI 感知缩放,但在链接项目中看到了ScaleSize扩展。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM