xsl
xsl

Reputation: 17416

Zooming a large picture with Windows Forms

I have to display a large image inside a windows forms application. The user should have the possibility to mark an area of the image, which should be then be zoomed like in the example illustrated below.

Zoom illustration

As mentioned before the image will be quite large, so my question is: Is it possible to achieve this with the default PictureBox control or am I better of using a 3rd party control? If so, please recommend a library which contains a control that offers these features.

As promised, here is the source of the control I made:

/// <summary>
/// A panel used to display an image and zoom into areas of the displayed
/// image.
/// </summary>
public sealed class PictureZoomPanel : Panel
{
    // The image to dispay, set in the Image property
    private Image _image;
    // The current zoom factor
    private float _zoom = 1;
    // The zoom rectangle on the panel.
    private Rectangle _panelZoomRect;
    // _panelZoomRect on the actual image
    private Rectangle? _imageZoomRect;
    // Used in the mouse event handlers
    private bool _mouseDown;
    // The pen used to draw the zoom rectangle
    private Pen _zoomPen;

    /// <summary>
    /// Create a new <see cref="PictureZoomPanel"/>
    /// </summary>
    public PictureZoomPanel()
    {
        // To prevent flickering
        DoubleBuffered = true;
        // To make resizing smoother
        ResizeRedraw = true;
        // Set default zoom pen
        ZoomPen = null;
    }

    /// <summary>
    /// The image to be displayed
    /// </summary>
    [Category("Appearance"), 
     Description("The image to be displayed.")]
    public Image Image
    {
        get { return _image; }
        set
        {
            _image = value;
            ZoomToFit();
        }
    }

    /// <summary>
    /// The pen used to draw the zoom rectangle.
    /// </summary>
    [Category("Appearance"), 
     Description("The pen used to draw the zoom rectangle.")]
    public Pen ZoomPen
    {
        get { return _zoomPen; }
        set {
            _zoomPen = value ?? new Pen(Color.Green, 2);
        }
    }

    /// <summary>
    /// Sets the zoom to a value where the whole image is visible.
    /// </summary>
    public void ZoomToFit()
    {
        _imageZoomRect = null;
        _mouseDown = false;
        _zoom = 1;

        // If no image is present, there is nothing further to do
        if (_image == null)
            return;

        var widthZoom = (float) Width / _image.Width;
        var heightZoom = (float) Height / _image.Height;

        // Make sure the whole image is visible
        _zoom = widthZoom < heightZoom ? widthZoom : heightZoom;

        // Force redraw
        Invalidate();
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        if (_image == null)
            return;

        _mouseDown = true;
        _panelZoomRect = new Rectangle(e.X, e.Y, 0, 0);
    }

    protected override void OnMouseUp(MouseEventArgs e)
    {
        if (_image == null || !_mouseDown)
            return;

        _mouseDown = false;

        // Without this, doubling clicking the control would cause zoom
        if (_panelZoomRect.Height == 0 || _panelZoomRect.Width == 0)
            return;

        // Tell the paint method to zoom
        _imageZoomRect = CalculateImageZoomRectangle();
        _zoom = RecalculateZoom();
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (_image == null)
            return;

        // This makes sure that the left mouse button is pressed.
        if (e.Button == MouseButtons.Left)
        {
            // Draws the rectangle as the mouse moves
            _panelZoomRect = new Rectangle(
                _panelZoomRect.Left, 
                _panelZoomRect.Top, 
                e.X - _panelZoomRect.Left, 
                e.Y - _panelZoomRect.Top);
        }

        // Force redraw to make sure the zoomRegion is painted
        Invalidate();
    }

    private Rectangle CalculateImageZoomRectangle()
    {
        // Calculate all the coordinates to required to transform
        var topLeft = new Point(_panelZoomRect.X, 
            _panelZoomRect.Y);
        var topRight = new Point(_panelZoomRect.X + _panelZoomRect.Width, 
            _panelZoomRect.Y);
        var bottomLeft = new Point(_panelZoomRect.X,
            _panelZoomRect.Y - _panelZoomRect.Height);
        var bottomRight = new Point(_panelZoomRect.X + _panelZoomRect.Height,
            _panelZoomRect.Y - _panelZoomRect.Height);

        var points = new [] { topLeft, topRight, bottomLeft, bottomRight };

        // Converts the points from panel to image position
        var mx = new Matrix(_zoom, 0, 0, _zoom, 0, 0);
        mx.Invert();
        mx.TransformPoints(points);

        var rectangleWidth = points[1].X - points[0].X;
        var rectangleHeight = points[0].Y - points[2].Y;

        // _imageZoom != null, means that we are zooming in on an
        // already zoomed in image. We must add the original values
        // to zoom in deeper
        return _imageZoomRect == null
            ? new Rectangle(points[0].X, 
                points[0].Y, 
                rectangleWidth,
                rectangleHeight)
            : new Rectangle(points[0].X + _imageZoomRect.Value.X,
                points[0].Y + _imageZoomRect.Value.Y, 
                rectangleWidth,
                rectangleHeight);
    }

    private float RecalculateZoom()
    {
        if (!_imageZoomRect.HasValue)
            return _zoom;

        var widthZoom = (float)Width / _imageZoomRect.Value.Width;
        var heightZoom = (float)Height / _imageZoomRect.Value.Height;

        return widthZoom < heightZoom ? widthZoom : heightZoom;
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        if (_image == null)
        {
            OnPaintBackground(e);
            return;
        }

        e.Graphics.Transform = new Matrix(_zoom, 0, 0, _zoom, 0, 0);

        // Turn of interpolation when zoomed
        e.Graphics.InterpolationMode = _imageZoomRect != null 
            ? InterpolationMode.NearestNeighbor 
            : InterpolationMode.Default;

        DrawImage(e);

        if (_mouseDown)
            DrawZoomRectangle(e);

        base.OnPaint(e);
    }

    private void DrawImage(PaintEventArgs e)
    {
        var destRec = !_imageZoomRect.HasValue
            ? new Rectangle(0, 0, _image.Width, _image.Height)
            : new Rectangle(0, 0, _imageZoomRect.Value.Width, 
                _imageZoomRect.Value.Height);

        var sourceRec = !_imageZoomRect.HasValue
            ? new Rectangle(0, 0, _image.Width, _image.Height)
            : _imageZoomRect.Value;

        e.Graphics.DrawImage(_image, destRec, 
            sourceRec.Location.X, sourceRec.Location.Y,
            sourceRec.Width, sourceRec.Height,
            GraphicsUnit.Pixel);
    }

    private void DrawZoomRectangle(PaintEventArgs e)
    {
        e.Graphics.Transform = new Matrix();
        e.Graphics.DrawRectangle(_zoomPen, _panelZoomRect);
    }
}

Upvotes: 5

Views: 4660

Answers (2)

LarsTech
LarsTech

Reputation: 81610

You can achieve zooming through translating the graphic object. Instead of using a PictureBox, I think a double buffered Panel is the better tool of choice.

You handle the paint event, and adjust the matrix presentation of it from there. Also, you need to adjust the AutoScrollMinSize property to have the scrollbars represent the correct range of the scaled image.

Quick example:

Bitmap bmp = new Bitmap(@"c:\myimage.png");
int zoom = 2;

private void Form1_Load(object sender, EventArgs e) {
  panel1.AutoScrollMinSize = new Size(bmp.Width * zoom, bmp.Height * zoom);
}

private void panel1_Paint(object sender, PaintEventArgs e) {
  using (Matrix mx = new Matrix(zoom, 0, 0, zoom, 0, 0)) {
    mx.Translate(panel1.AutoScrollPosition.X / zoom, panel1.AutoScrollPosition.Y / zoom);
    e.Graphics.Transform = mx;
    e.Graphics.DrawImage(bmp, new Point(0, 0));
  }
}

This method is for tracking the mouse movement of a scaled image:

protected Point BacktrackMouse(MouseEventArgs e)
{
  Matrix mx = new Matrix(_zoom, 0, 0, _zoom, 0, 0);
  mx.Translate(this.AutoScrollPosition.X * (1.0f / zoom), 
               this.AutoScrollPosition.Y * (1.0f / zoom));
  mx.Invert();
  Point[] p = new Point[] { new Point(e.X, e.Y) };
  mx.TransformPoints(p);
  return p[0];
}

Upvotes: 3

silverspoon
silverspoon

Reputation: 1095

Try the following links - they may not be the end solution for you but a part of them should help you to build your own:

  1. Pan and Zoom Very Large Images
  2. Image Target Zoom (Pan Zoom)
  3. A scrollable, zoomable, and scalable picture box

Upvotes: 2

Related Questions